apaas-oapi-client 0.1.39 → 1.0.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.
- package/README.md +52 -8
- package/UserManual.md +115 -1
- package/dist/field-schema-rules.d.ts +62 -0
- package/dist/field-types.d.ts +2 -2
- package/dist/index.d.ts +332 -3
- package/dist/index.js +1383 -4
- package/dist/schema-utils.d.ts +194 -0
- package/package.json +3 -3
- package/skills/apaas-function-flow/SKILL.md +47 -2
- package/skills/apaas-function-flow/agents/openai.yaml +2 -2
- package/skills/apaas-object/SKILL.md +41 -2
- package/skills/apaas-object/agents/openai.yaml +2 -2
- package/skills/apaas-object/references/id-cursor-pagination.md +64 -0
- package/skills/apaas-schema/SKILL.md +40 -24
- package/skills/apaas-schema/references/field-schema-rules.md +70 -0
- package/skills/apaas-schema/references/schema-maintenance-sop.md +157 -0
- package/skills/apaas-shared/SKILL.md +5 -2
- package/skills/apaas-shared/agents/openai.yaml +2 -2
- package/skills/apaas-shared/references/openapi-coverage.md +33 -0
- package/skills/apaas-shared/references/openapi-error-codes.md +55 -0
- package/src/FIELD_SCHEMA_RULES.md +38 -0
- package/src/field-schema-rules.ts +130 -1
- package/src/field-types.ts +2 -2
- package/src/index.ts +867 -7
- package/src/schema-utils.ts +792 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateFieldDefinition,
|
|
3
|
+
MultilingualText,
|
|
4
|
+
ObjectSettings,
|
|
5
|
+
UpdateFieldDefinition
|
|
6
|
+
} from './field-types';
|
|
7
|
+
import { normalizeOptionColorForSchema } from './field-schema-rules';
|
|
8
|
+
|
|
9
|
+
export const SCHEMA_BATCH_SIZE = 10;
|
|
10
|
+
|
|
11
|
+
export type SchemaResponseFailureLayer = 'request' | 'silent' | 'item';
|
|
12
|
+
|
|
13
|
+
export interface SchemaResponseItem {
|
|
14
|
+
api_name?: string;
|
|
15
|
+
apiName?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
status?: {
|
|
18
|
+
code?: string | number;
|
|
19
|
+
message?: string;
|
|
20
|
+
msg?: string;
|
|
21
|
+
};
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SchemaResponse {
|
|
26
|
+
code?: string | number;
|
|
27
|
+
msg?: string;
|
|
28
|
+
message?: string;
|
|
29
|
+
data?: {
|
|
30
|
+
items?: SchemaResponseItem[];
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
} | null;
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SchemaResponseFailure {
|
|
37
|
+
layer: SchemaResponseFailureLayer;
|
|
38
|
+
context: string;
|
|
39
|
+
code?: string;
|
|
40
|
+
message: string;
|
|
41
|
+
apiName?: string;
|
|
42
|
+
item?: SchemaResponseItem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SchemaResponseValidationOptions {
|
|
46
|
+
requireItemStatus?: boolean;
|
|
47
|
+
allowDataNull?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SchemaResponseValidationResult<T extends SchemaResponse = SchemaResponse> {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
result: T;
|
|
53
|
+
failures: SchemaResponseFailure[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SchemaBatchInfo {
|
|
57
|
+
batchIndex: number;
|
|
58
|
+
batchCount: number;
|
|
59
|
+
start: number;
|
|
60
|
+
end: number;
|
|
61
|
+
batchSize: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SchemaBatchExecuteOptions {
|
|
65
|
+
batchSize?: number;
|
|
66
|
+
context?: string;
|
|
67
|
+
continueOnError?: boolean;
|
|
68
|
+
checkResponse?: boolean;
|
|
69
|
+
responseOptions?: SchemaResponseValidationOptions;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SchemaBatchExecuteResult<R extends SchemaResponse = SchemaResponse> {
|
|
73
|
+
ok: boolean;
|
|
74
|
+
total: number;
|
|
75
|
+
batchSize: number;
|
|
76
|
+
batchCount: number;
|
|
77
|
+
results: R[];
|
|
78
|
+
failures: SchemaResponseFailure[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type SchemaStageFieldDefinition = (CreateFieldDefinition | UpdateFieldDefinition) & {
|
|
82
|
+
operator?: 'add' | 'replace' | 'remove';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export interface SchemaManagedObjectDefinition {
|
|
86
|
+
api_name: string;
|
|
87
|
+
label: MultilingualText;
|
|
88
|
+
settings?: ObjectSettings;
|
|
89
|
+
fields?: SchemaStageFieldDefinition[];
|
|
90
|
+
[key: string]: any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SchemaCreateShellsOptions extends SchemaBatchExecuteOptions {
|
|
94
|
+
skipExisting?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SchemaAddFieldsOptions {
|
|
98
|
+
context?: string;
|
|
99
|
+
skipExisting?: boolean;
|
|
100
|
+
checkResponse?: boolean;
|
|
101
|
+
responseOptions?: SchemaResponseValidationOptions;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SchemaAddFieldsResult {
|
|
105
|
+
objectName: string;
|
|
106
|
+
addedFields: string[];
|
|
107
|
+
skippedFields: string[];
|
|
108
|
+
result?: SchemaResponse;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface SchemaCreateShellsResult {
|
|
112
|
+
requested: string[];
|
|
113
|
+
created: string[];
|
|
114
|
+
skippedExisting: string[];
|
|
115
|
+
batches?: SchemaBatchExecuteResult;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface SchemaCreateObjectsInStagesOptions extends SchemaBatchExecuteOptions {
|
|
119
|
+
skipExisting?: boolean;
|
|
120
|
+
updateFinalSettings?: boolean;
|
|
121
|
+
verify?: boolean;
|
|
122
|
+
includeMarkdown?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SchemaCreateObjectsInStagesResult {
|
|
126
|
+
shells: SchemaCreateShellsResult;
|
|
127
|
+
baseFields: SchemaAddFieldsResult[];
|
|
128
|
+
lookupFields: SchemaAddFieldsResult[];
|
|
129
|
+
referenceFields: SchemaAddFieldsResult[];
|
|
130
|
+
finalSettings?: SchemaBatchExecuteResult;
|
|
131
|
+
verification?: SchemaVerificationResult;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface SchemaObjectVerification {
|
|
135
|
+
objectName: string;
|
|
136
|
+
exists: boolean;
|
|
137
|
+
fields: any[];
|
|
138
|
+
customFields: any[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SchemaVerificationResult {
|
|
142
|
+
objects: SchemaObjectVerification[];
|
|
143
|
+
markdown?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SchemaVerificationOptions {
|
|
147
|
+
includeMarkdown?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface SchemaFieldRemovalPlan {
|
|
151
|
+
referenceFieldObjects: Array<{ api_name: string; fields: Array<{ operator: 'remove'; api_name: string }> }>;
|
|
152
|
+
lookupObjects: Array<{ api_name: string; fields: Array<{ operator: 'remove'; api_name: string }> }>;
|
|
153
|
+
otherFieldObjects: Array<{ api_name: string; fields: Array<{ operator: 'remove'; api_name: string }> }>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface DeleteAllCustomObjectsOptions extends SchemaBatchExecuteOptions {
|
|
157
|
+
confirm?: boolean;
|
|
158
|
+
removeOtherFields?: boolean;
|
|
159
|
+
verify?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface DeleteAllCustomObjectsResult {
|
|
163
|
+
deletedObjects: string[];
|
|
164
|
+
removalPlan: SchemaFieldRemovalPlan;
|
|
165
|
+
fieldRemovalResults: SchemaBatchExecuteResult[];
|
|
166
|
+
deleteResults?: SchemaBatchExecuteResult;
|
|
167
|
+
remainingObjects?: string[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface SchemaClientLike {
|
|
171
|
+
schema: {
|
|
172
|
+
create(params: any): Promise<SchemaResponse>;
|
|
173
|
+
update(params: any): Promise<SchemaResponse>;
|
|
174
|
+
delete(params: any): Promise<SchemaResponse>;
|
|
175
|
+
};
|
|
176
|
+
object: {
|
|
177
|
+
listWithIterator(params?: any): Promise<{ items?: any[] }>;
|
|
178
|
+
metadata: {
|
|
179
|
+
fields(params: { object_name: string }): Promise<any>;
|
|
180
|
+
export2markdown?(options?: { object_names?: string[] }): Promise<string>;
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function isSystemSchemaName(apiName: string): boolean {
|
|
186
|
+
return apiName.startsWith('_');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function normalizeSchemaObjectsForWrite<T extends { fields?: any[] }>(objects: T[]): T[] {
|
|
190
|
+
return objects.map((object) => {
|
|
191
|
+
if (!object.fields) {
|
|
192
|
+
return { ...object };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...object,
|
|
197
|
+
fields: object.fields.map((field) => normalizeSchemaFieldForWrite(field))
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeSchemaFieldForWrite<T extends { type?: any }>(field: T): T {
|
|
203
|
+
const settings = field.type?.settings;
|
|
204
|
+
if (field.type?.name !== 'enum' || !Array.isArray(settings?.options)) {
|
|
205
|
+
return { ...field };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
...field,
|
|
210
|
+
type: {
|
|
211
|
+
...field.type,
|
|
212
|
+
settings: {
|
|
213
|
+
...settings,
|
|
214
|
+
options: settings.options.map((option: any) => {
|
|
215
|
+
if (!option || typeof option !== 'object' || !('color' in option)) {
|
|
216
|
+
return option;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
...option,
|
|
221
|
+
color: normalizeOptionColorForSchema(option.color)
|
|
222
|
+
};
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function validateSchemaResponse<T extends SchemaResponse>(
|
|
230
|
+
result: T,
|
|
231
|
+
context = 'schema',
|
|
232
|
+
options: SchemaResponseValidationOptions = {}
|
|
233
|
+
): SchemaResponseValidationResult<T> {
|
|
234
|
+
const failures: SchemaResponseFailure[] = [];
|
|
235
|
+
|
|
236
|
+
if (!result) {
|
|
237
|
+
failures.push({
|
|
238
|
+
layer: 'request',
|
|
239
|
+
context,
|
|
240
|
+
code: 'NO_RESULT',
|
|
241
|
+
message: `${context} request failed: empty response`
|
|
242
|
+
});
|
|
243
|
+
return { ok: false, result, failures };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (String(result.code) !== '0') {
|
|
247
|
+
failures.push({
|
|
248
|
+
layer: 'request',
|
|
249
|
+
context,
|
|
250
|
+
code: result.code === undefined ? 'undefined' : String(result.code),
|
|
251
|
+
message: `${context} request failed: ${result.code} ${result.msg || result.message || ''}`.trim()
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (result.data === null && !options.allowDataNull) {
|
|
256
|
+
failures.push({
|
|
257
|
+
layer: 'silent',
|
|
258
|
+
context,
|
|
259
|
+
code: 'DATA_NULL',
|
|
260
|
+
message: `${context} silently failed: result.code is 0 but result.data is null`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const item of result.data?.items || []) {
|
|
265
|
+
const itemCode = item.status?.code;
|
|
266
|
+
if (itemCode === undefined && options.requireItemStatus) {
|
|
267
|
+
failures.push({
|
|
268
|
+
layer: 'item',
|
|
269
|
+
context,
|
|
270
|
+
code: 'MISSING_STATUS',
|
|
271
|
+
apiName: getItemApiName(item),
|
|
272
|
+
item,
|
|
273
|
+
message: `${context} item status is missing: ${getItemApiName(item) || 'unknown'}`
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (itemCode !== undefined && String(itemCode) !== '0') {
|
|
279
|
+
failures.push({
|
|
280
|
+
layer: 'item',
|
|
281
|
+
context,
|
|
282
|
+
code: String(itemCode),
|
|
283
|
+
apiName: getItemApiName(item),
|
|
284
|
+
item,
|
|
285
|
+
message: `${context} item failed: ${getItemApiName(item) || 'unknown'} ${item.status?.message || item.status?.msg || ''}`.trim()
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { ok: failures.length === 0, result, failures };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function checkSchemaResponse<T extends SchemaResponse>(
|
|
294
|
+
result: T,
|
|
295
|
+
context = 'schema',
|
|
296
|
+
options?: SchemaResponseValidationOptions
|
|
297
|
+
): T {
|
|
298
|
+
const validation = validateSchemaResponse(result, context, options);
|
|
299
|
+
if (!validation.ok) {
|
|
300
|
+
const error = new Error(validation.failures.map((failure) => failure.message).join('; '));
|
|
301
|
+
(error as Error & { failures?: SchemaResponseFailure[] }).failures = validation.failures;
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function batchExecute<T, R extends SchemaResponse>(
|
|
308
|
+
items: T[],
|
|
309
|
+
fn: (batch: T[], info: SchemaBatchInfo) => Promise<R>,
|
|
310
|
+
options: SchemaBatchExecuteOptions = {}
|
|
311
|
+
): Promise<SchemaBatchExecuteResult<R>> {
|
|
312
|
+
const batchSize = options.batchSize ?? SCHEMA_BATCH_SIZE;
|
|
313
|
+
if (!Number.isInteger(batchSize) || batchSize <= 0) {
|
|
314
|
+
throw new Error('batchSize must be a positive integer.');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const batchCount = Math.ceil(items.length / batchSize);
|
|
318
|
+
const results: R[] = [];
|
|
319
|
+
const failures: SchemaResponseFailure[] = [];
|
|
320
|
+
|
|
321
|
+
for (let start = 0; start < items.length; start += batchSize) {
|
|
322
|
+
const batch = items.slice(start, start + batchSize);
|
|
323
|
+
const batchIndex = Math.floor(start / batchSize) + 1;
|
|
324
|
+
const info: SchemaBatchInfo = {
|
|
325
|
+
batchIndex,
|
|
326
|
+
batchCount,
|
|
327
|
+
start,
|
|
328
|
+
end: start + batch.length,
|
|
329
|
+
batchSize: batch.length
|
|
330
|
+
};
|
|
331
|
+
const context = `${options.context || 'schema batch'} [${batchIndex}/${batchCount}]`;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const result = await fn(batch, info);
|
|
335
|
+
results.push(result);
|
|
336
|
+
|
|
337
|
+
if (options.checkResponse !== false) {
|
|
338
|
+
const validation = validateSchemaResponse(result, context, options.responseOptions);
|
|
339
|
+
failures.push(...validation.failures);
|
|
340
|
+
if (!validation.ok && !options.continueOnError) {
|
|
341
|
+
const error = new Error(validation.failures.map((failure) => failure.message).join('; '));
|
|
342
|
+
(error as Error & { failures?: SchemaResponseFailure[] }).failures = validation.failures;
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (!options.continueOnError) {
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
failures.push({
|
|
351
|
+
layer: 'request',
|
|
352
|
+
context,
|
|
353
|
+
code: 'EXCEPTION',
|
|
354
|
+
message: error instanceof Error ? error.message : String(error)
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
ok: failures.length === 0,
|
|
361
|
+
total: items.length,
|
|
362
|
+
batchSize,
|
|
363
|
+
batchCount,
|
|
364
|
+
results,
|
|
365
|
+
failures
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function splitSchemaFieldsByDependency(fields: SchemaStageFieldDefinition[]): {
|
|
370
|
+
baseFields: SchemaStageFieldDefinition[];
|
|
371
|
+
lookupFields: SchemaStageFieldDefinition[];
|
|
372
|
+
referenceFields: SchemaStageFieldDefinition[];
|
|
373
|
+
} {
|
|
374
|
+
const baseFields: SchemaStageFieldDefinition[] = [];
|
|
375
|
+
const lookupFields: SchemaStageFieldDefinition[] = [];
|
|
376
|
+
const referenceFields: SchemaStageFieldDefinition[] = [];
|
|
377
|
+
|
|
378
|
+
for (const field of fields) {
|
|
379
|
+
const typeName = getSchemaFieldTypeName(field);
|
|
380
|
+
if (typeName === 'reference_field' || typeName === 'referenceField') {
|
|
381
|
+
referenceFields.push(field);
|
|
382
|
+
} else if (typeName === 'lookup' || typeName === 'lookup_multi') {
|
|
383
|
+
lookupFields.push(field);
|
|
384
|
+
} else {
|
|
385
|
+
baseFields.push(field);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return { baseFields, lookupFields, referenceFields };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function createSchemaObjectShells(
|
|
393
|
+
client: SchemaClientLike,
|
|
394
|
+
objects: SchemaManagedObjectDefinition[],
|
|
395
|
+
options: SchemaCreateShellsOptions = {}
|
|
396
|
+
): Promise<SchemaCreateShellsResult> {
|
|
397
|
+
assertNoSystemObjects(objects.map((object) => object.api_name), 'createSchemaObjectShells');
|
|
398
|
+
|
|
399
|
+
const existingNames = options.skipExisting === false
|
|
400
|
+
? new Set<string>()
|
|
401
|
+
: await getExistingObjectNames(client);
|
|
402
|
+
const shells = objects
|
|
403
|
+
.filter((object) => !existingNames.has(object.api_name))
|
|
404
|
+
.map(toShellObject);
|
|
405
|
+
|
|
406
|
+
const result: SchemaCreateShellsResult = {
|
|
407
|
+
requested: objects.map((object) => object.api_name),
|
|
408
|
+
created: shells.map((object) => object.api_name),
|
|
409
|
+
skippedExisting: objects
|
|
410
|
+
.filter((object) => existingNames.has(object.api_name))
|
|
411
|
+
.map((object) => object.api_name)
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
if (shells.length === 0) {
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
result.batches = await batchExecute(
|
|
419
|
+
shells,
|
|
420
|
+
(batch) => client.schema.create({ objects: batch }),
|
|
421
|
+
{
|
|
422
|
+
batchSize: options.batchSize,
|
|
423
|
+
context: options.context || 'schema.createShells',
|
|
424
|
+
continueOnError: options.continueOnError,
|
|
425
|
+
checkResponse: options.checkResponse,
|
|
426
|
+
responseOptions: options.responseOptions
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export async function addFieldsIdempotent(
|
|
434
|
+
client: SchemaClientLike,
|
|
435
|
+
objectName: string,
|
|
436
|
+
fieldsToAdd: SchemaStageFieldDefinition[],
|
|
437
|
+
options: SchemaAddFieldsOptions = {}
|
|
438
|
+
): Promise<SchemaAddFieldsResult> {
|
|
439
|
+
assertNoSystemObjects([objectName], 'addFieldsIdempotent');
|
|
440
|
+
assertNoSystemFields(fieldsToAdd.map((field) => field.api_name), `addFieldsIdempotent(${objectName})`);
|
|
441
|
+
|
|
442
|
+
const existingNames = options.skipExisting === false
|
|
443
|
+
? new Set<string>()
|
|
444
|
+
: await getExistingFieldNames(client, objectName);
|
|
445
|
+
const newFields = fieldsToAdd.filter((field) => !existingNames.has(field.api_name)).map(toAddField);
|
|
446
|
+
const skippedFields = fieldsToAdd
|
|
447
|
+
.filter((field) => existingNames.has(field.api_name))
|
|
448
|
+
.map((field) => field.api_name);
|
|
449
|
+
|
|
450
|
+
if (newFields.length === 0) {
|
|
451
|
+
return {
|
|
452
|
+
objectName,
|
|
453
|
+
addedFields: [],
|
|
454
|
+
skippedFields
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const result = await client.schema.update({
|
|
459
|
+
objects: [{ api_name: objectName, fields: newFields }]
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (options.checkResponse !== false) {
|
|
463
|
+
checkSchemaResponse(result, options.context || `schema.addFieldsIdempotent(${objectName})`, options.responseOptions);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
objectName,
|
|
468
|
+
addedFields: newFields.map((field) => field.api_name),
|
|
469
|
+
skippedFields,
|
|
470
|
+
result
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function createSchemaObjectsInStages(
|
|
475
|
+
client: SchemaClientLike,
|
|
476
|
+
objects: SchemaManagedObjectDefinition[],
|
|
477
|
+
options: SchemaCreateObjectsInStagesOptions = {}
|
|
478
|
+
): Promise<SchemaCreateObjectsInStagesResult> {
|
|
479
|
+
const context = options.context || 'schema.createWithStages';
|
|
480
|
+
const shells = await createSchemaObjectShells(client, objects, {
|
|
481
|
+
...options,
|
|
482
|
+
context: `${context}.shells`
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const baseFields: SchemaAddFieldsResult[] = [];
|
|
486
|
+
const lookupFields: SchemaAddFieldsResult[] = [];
|
|
487
|
+
const referenceFields: SchemaAddFieldsResult[] = [];
|
|
488
|
+
|
|
489
|
+
for (const object of objects) {
|
|
490
|
+
assertNoSystemFields((object.fields || []).map((field) => field.api_name), `${context}(${object.api_name})`);
|
|
491
|
+
const split = splitSchemaFieldsByDependency(object.fields || []);
|
|
492
|
+
|
|
493
|
+
if (split.baseFields.length > 0) {
|
|
494
|
+
baseFields.push(await addFieldsIdempotent(client, object.api_name, split.baseFields, {
|
|
495
|
+
context: `${context}.baseFields(${object.api_name})`,
|
|
496
|
+
skipExisting: options.skipExisting,
|
|
497
|
+
checkResponse: options.checkResponse,
|
|
498
|
+
responseOptions: options.responseOptions
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (split.lookupFields.length > 0) {
|
|
503
|
+
lookupFields.push(await addFieldsIdempotent(client, object.api_name, split.lookupFields, {
|
|
504
|
+
context: `${context}.lookupFields(${object.api_name})`,
|
|
505
|
+
skipExisting: options.skipExisting,
|
|
506
|
+
checkResponse: options.checkResponse,
|
|
507
|
+
responseOptions: options.responseOptions
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (split.referenceFields.length > 0) {
|
|
512
|
+
referenceFields.push(await addFieldsIdempotent(client, object.api_name, split.referenceFields, {
|
|
513
|
+
context: `${context}.referenceFields(${object.api_name})`,
|
|
514
|
+
skipExisting: options.skipExisting,
|
|
515
|
+
checkResponse: options.checkResponse,
|
|
516
|
+
responseOptions: options.responseOptions
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const result: SchemaCreateObjectsInStagesResult = {
|
|
522
|
+
shells,
|
|
523
|
+
baseFields,
|
|
524
|
+
lookupFields,
|
|
525
|
+
referenceFields
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const finalSettings = options.updateFinalSettings === false
|
|
529
|
+
? []
|
|
530
|
+
: objects
|
|
531
|
+
.filter((object) => object.settings && Object.keys(object.settings).length > 0)
|
|
532
|
+
.map((object) => ({ api_name: object.api_name, settings: object.settings }));
|
|
533
|
+
|
|
534
|
+
if (finalSettings.length > 0) {
|
|
535
|
+
result.finalSettings = await batchExecute(
|
|
536
|
+
finalSettings,
|
|
537
|
+
(batch) => client.schema.update({ objects: batch }),
|
|
538
|
+
{
|
|
539
|
+
batchSize: options.batchSize,
|
|
540
|
+
context: `${context}.finalSettings`,
|
|
541
|
+
continueOnError: options.continueOnError,
|
|
542
|
+
checkResponse: options.checkResponse,
|
|
543
|
+
responseOptions: {
|
|
544
|
+
...options.responseOptions,
|
|
545
|
+
allowDataNull: true
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (options.verify !== false) {
|
|
552
|
+
result.verification = await verifySchemaObjects(
|
|
553
|
+
client,
|
|
554
|
+
objects.map((object) => object.api_name),
|
|
555
|
+
{ includeMarkdown: options.includeMarkdown }
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export async function verifySchemaObjects(
|
|
563
|
+
client: SchemaClientLike,
|
|
564
|
+
objectNames: string[],
|
|
565
|
+
options: SchemaVerificationOptions = {}
|
|
566
|
+
): Promise<SchemaVerificationResult> {
|
|
567
|
+
const allObjects = await client.object.listWithIterator();
|
|
568
|
+
const objects: SchemaObjectVerification[] = [];
|
|
569
|
+
|
|
570
|
+
for (const objectName of objectNames) {
|
|
571
|
+
const exists = Boolean(allObjects.items?.find((object: any) => object.apiName === objectName || object.api_name === objectName));
|
|
572
|
+
if (!exists) {
|
|
573
|
+
objects.push({
|
|
574
|
+
objectName,
|
|
575
|
+
exists: false,
|
|
576
|
+
fields: [],
|
|
577
|
+
customFields: []
|
|
578
|
+
});
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const fieldResult = await client.object.metadata.fields({ object_name: objectName });
|
|
583
|
+
if (fieldResult.code !== undefined && String(fieldResult.code) !== '0') {
|
|
584
|
+
throw new Error(`verifySchemaObjects(${objectName}) failed: ${fieldResult.code} ${fieldResult.msg || fieldResult.message || ''}`.trim());
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const fields = fieldResult.data?.fields || [];
|
|
588
|
+
objects.push({
|
|
589
|
+
objectName,
|
|
590
|
+
exists: true,
|
|
591
|
+
fields,
|
|
592
|
+
customFields: fields.filter((field: any) => !isSystemSchemaName(field.apiName || field.api_name || ''))
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const result: SchemaVerificationResult = { objects };
|
|
597
|
+
if (options.includeMarkdown && client.object.metadata.export2markdown) {
|
|
598
|
+
result.markdown = await client.object.metadata.export2markdown({ object_names: objectNames });
|
|
599
|
+
}
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function buildFieldRemovalPlan(fieldsByObject: Record<string, any[]>): SchemaFieldRemovalPlan {
|
|
604
|
+
const referenceFieldObjects = new Map<string, Array<{ operator: 'remove'; api_name: string }>>();
|
|
605
|
+
const lookupObjects = new Map<string, Array<{ operator: 'remove'; api_name: string }>>();
|
|
606
|
+
const otherFieldObjects = new Map<string, Array<{ operator: 'remove'; api_name: string }>>();
|
|
607
|
+
|
|
608
|
+
for (const [objectName, fields] of Object.entries(fieldsByObject)) {
|
|
609
|
+
for (const field of fields) {
|
|
610
|
+
const apiName = field.api_name || field.apiName;
|
|
611
|
+
if (!apiName || isSystemSchemaName(apiName)) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const target = getRemovalGroup(field, referenceFieldObjects, lookupObjects, otherFieldObjects);
|
|
616
|
+
const existing = target.get(objectName) || [];
|
|
617
|
+
existing.push({ operator: 'remove', api_name: apiName });
|
|
618
|
+
target.set(objectName, existing);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
referenceFieldObjects: mapRemovalObjects(referenceFieldObjects),
|
|
624
|
+
lookupObjects: mapRemovalObjects(lookupObjects),
|
|
625
|
+
otherFieldObjects: mapRemovalObjects(otherFieldObjects)
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export async function deleteAllCustomObjects(
|
|
630
|
+
client: SchemaClientLike,
|
|
631
|
+
options: DeleteAllCustomObjectsOptions = {}
|
|
632
|
+
): Promise<DeleteAllCustomObjectsResult> {
|
|
633
|
+
if (options.confirm !== true) {
|
|
634
|
+
throw new Error('deleteAllCustomObjects requires { confirm: true }.');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const context = options.context || 'schema.deleteAllCustomObjects';
|
|
638
|
+
const allObjects = await client.object.listWithIterator();
|
|
639
|
+
const customObjects = (allObjects.items || []).filter((object: any) => !isSystemSchemaName(object.apiName || object.api_name || ''));
|
|
640
|
+
const objectNames = customObjects.map((object: any) => object.apiName || object.api_name);
|
|
641
|
+
const fieldsByObject: Record<string, any[]> = {};
|
|
642
|
+
|
|
643
|
+
for (const objectName of objectNames) {
|
|
644
|
+
const fieldResult = await client.object.metadata.fields({ object_name: objectName });
|
|
645
|
+
if (fieldResult.code !== undefined && String(fieldResult.code) !== '0') {
|
|
646
|
+
throw new Error(`${context}.readFields(${objectName}) failed: ${fieldResult.code} ${fieldResult.msg || fieldResult.message || ''}`.trim());
|
|
647
|
+
}
|
|
648
|
+
fieldsByObject[objectName] = fieldResult.data?.fields || [];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const removalPlan = buildFieldRemovalPlan(fieldsByObject);
|
|
652
|
+
const fieldRemovalResults: SchemaBatchExecuteResult[] = [];
|
|
653
|
+
|
|
654
|
+
for (const [phase, updateObjects] of [
|
|
655
|
+
['referenceFields', removalPlan.referenceFieldObjects],
|
|
656
|
+
['lookupFields', removalPlan.lookupObjects],
|
|
657
|
+
['otherFields', options.removeOtherFields ? removalPlan.otherFieldObjects : []]
|
|
658
|
+
] as Array<[string, SchemaFieldRemovalPlan['referenceFieldObjects']]>) {
|
|
659
|
+
if (updateObjects.length === 0) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
fieldRemovalResults.push(await batchExecute(
|
|
664
|
+
updateObjects,
|
|
665
|
+
(batch) => client.schema.update({ objects: batch }),
|
|
666
|
+
{
|
|
667
|
+
batchSize: options.batchSize,
|
|
668
|
+
context: `${context}.${phase}`,
|
|
669
|
+
continueOnError: options.continueOnError,
|
|
670
|
+
checkResponse: options.checkResponse,
|
|
671
|
+
responseOptions: options.responseOptions
|
|
672
|
+
}
|
|
673
|
+
));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const result: DeleteAllCustomObjectsResult = {
|
|
677
|
+
deletedObjects: objectNames,
|
|
678
|
+
removalPlan,
|
|
679
|
+
fieldRemovalResults
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
if (objectNames.length > 0) {
|
|
683
|
+
result.deleteResults = await batchExecute(
|
|
684
|
+
objectNames,
|
|
685
|
+
(batch) => client.schema.delete({ api_names: batch }),
|
|
686
|
+
{
|
|
687
|
+
batchSize: options.batchSize,
|
|
688
|
+
context: `${context}.deleteObjects`,
|
|
689
|
+
continueOnError: options.continueOnError,
|
|
690
|
+
checkResponse: options.checkResponse,
|
|
691
|
+
responseOptions: {
|
|
692
|
+
...options.responseOptions,
|
|
693
|
+
allowDataNull: true
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (options.verify !== false) {
|
|
700
|
+
const after = await client.object.listWithIterator();
|
|
701
|
+
result.remainingObjects = (after.items || [])
|
|
702
|
+
.filter((object: any) => objectNames.includes(object.apiName || object.api_name))
|
|
703
|
+
.map((object: any) => object.apiName || object.api_name);
|
|
704
|
+
if (result.remainingObjects.length > 0) {
|
|
705
|
+
throw new Error(`${context} verification failed: remaining objects ${result.remainingObjects.join(', ')}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function getItemApiName(item: SchemaResponseItem): string | undefined {
|
|
713
|
+
return item.api_name || item.apiName || item.name;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function getSchemaFieldTypeName(field: SchemaStageFieldDefinition | any): string | undefined {
|
|
717
|
+
const type = field.type;
|
|
718
|
+
if (typeof type === 'string') {
|
|
719
|
+
return type;
|
|
720
|
+
}
|
|
721
|
+
return type?.name;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function toAddField(field: SchemaStageFieldDefinition): SchemaStageFieldDefinition {
|
|
725
|
+
return {
|
|
726
|
+
...field,
|
|
727
|
+
operator: 'add'
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function toShellObject(object: SchemaManagedObjectDefinition): SchemaManagedObjectDefinition {
|
|
732
|
+
const { fields: _fields, ...shell } = object;
|
|
733
|
+
const originalSettings = object.settings || {};
|
|
734
|
+
return {
|
|
735
|
+
...shell,
|
|
736
|
+
settings: {
|
|
737
|
+
...originalSettings,
|
|
738
|
+
display_name: '_id',
|
|
739
|
+
allow_search_fields: ['_id'],
|
|
740
|
+
search_layout: []
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function getExistingObjectNames(client: SchemaClientLike): Promise<Set<string>> {
|
|
746
|
+
const result = await client.object.listWithIterator();
|
|
747
|
+
return new Set((result.items || []).map((object: any) => object.apiName || object.api_name).filter(Boolean));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function getExistingFieldNames(client: SchemaClientLike, objectName: string): Promise<Set<string>> {
|
|
751
|
+
const result = await client.object.metadata.fields({ object_name: objectName });
|
|
752
|
+
if (result.code !== undefined && String(result.code) !== '0') {
|
|
753
|
+
throw new Error(`getExistingFieldNames(${objectName}) failed: ${result.code} ${result.msg || result.message || ''}`.trim());
|
|
754
|
+
}
|
|
755
|
+
return new Set((result.data?.fields || []).map((field: any) => field.apiName || field.api_name).filter(Boolean));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function assertNoSystemObjects(apiNames: string[], context: string): void {
|
|
759
|
+
const systemNames = apiNames.filter(isSystemSchemaName);
|
|
760
|
+
if (systemNames.length > 0) {
|
|
761
|
+
throw new Error(`${context} cannot modify system objects: ${systemNames.join(', ')}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function assertNoSystemFields(apiNames: string[], context: string): void {
|
|
766
|
+
const systemNames = apiNames.filter(isSystemSchemaName);
|
|
767
|
+
if (systemNames.length > 0) {
|
|
768
|
+
throw new Error(`${context} cannot modify system fields: ${systemNames.join(', ')}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function getRemovalGroup(
|
|
773
|
+
field: any,
|
|
774
|
+
referenceFieldObjects: Map<string, Array<{ operator: 'remove'; api_name: string }>>,
|
|
775
|
+
lookupObjects: Map<string, Array<{ operator: 'remove'; api_name: string }>>,
|
|
776
|
+
otherFieldObjects: Map<string, Array<{ operator: 'remove'; api_name: string }>>
|
|
777
|
+
): Map<string, Array<{ operator: 'remove'; api_name: string }>> {
|
|
778
|
+
const typeName = getSchemaFieldTypeName(field);
|
|
779
|
+
if (typeName === 'reference_field' || typeName === 'referenceField') {
|
|
780
|
+
return referenceFieldObjects;
|
|
781
|
+
}
|
|
782
|
+
if (typeName === 'lookup' || typeName === 'lookup_multi') {
|
|
783
|
+
return lookupObjects;
|
|
784
|
+
}
|
|
785
|
+
return otherFieldObjects;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function mapRemovalObjects(
|
|
789
|
+
fieldsByObject: Map<string, Array<{ operator: 'remove'; api_name: string }>>
|
|
790
|
+
): Array<{ api_name: string; fields: Array<{ operator: 'remove'; api_name: string }> }> {
|
|
791
|
+
return Array.from(fieldsByObject.entries()).map(([api_name, fields]) => ({ api_name, fields }));
|
|
792
|
+
}
|