@nocobase/ctl 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +164 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +14 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +20 -0
  6. package/dist/commands/env/add.js +51 -0
  7. package/dist/commands/env/index.js +27 -0
  8. package/dist/commands/env/list.js +31 -0
  9. package/dist/commands/env/remove.js +54 -0
  10. package/dist/commands/env/update.js +54 -0
  11. package/dist/commands/env/use.js +26 -0
  12. package/dist/commands/resource/create.js +15 -0
  13. package/dist/commands/resource/destroy.js +15 -0
  14. package/dist/commands/resource/get.js +15 -0
  15. package/dist/commands/resource/index.js +7 -0
  16. package/dist/commands/resource/list.js +16 -0
  17. package/dist/commands/resource/query.js +15 -0
  18. package/dist/commands/resource/update.js +15 -0
  19. package/dist/generated/command-registry.js +81 -0
  20. package/dist/lib/api-client.js +196 -0
  21. package/dist/lib/auth-store.js +92 -0
  22. package/dist/lib/bootstrap.js +263 -0
  23. package/dist/lib/build-config.js +10 -0
  24. package/dist/lib/cli-home.js +30 -0
  25. package/dist/lib/generated-command.js +113 -0
  26. package/dist/lib/naming.js +70 -0
  27. package/dist/lib/openapi.js +254 -0
  28. package/dist/lib/post-processors.js +23 -0
  29. package/dist/lib/resource-command.js +331 -0
  30. package/dist/lib/resource-request.js +103 -0
  31. package/dist/lib/runtime-generator.js +383 -0
  32. package/dist/lib/runtime-store.js +56 -0
  33. package/dist/lib/ui.js +154 -0
  34. package/dist/post-processors/data-modeling.js +66 -0
  35. package/dist/post-processors/data-source-manager.js +114 -0
  36. package/dist/post-processors/index.js +19 -0
  37. package/nocobase-ctl.config.json +327 -0
  38. package/package.json +61 -0
@@ -0,0 +1,331 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { executeResourceRequest } from "./resource-request.js";
3
+ import { setVerboseMode } from "./ui.js";
4
+ function parseJson(value, flagName) {
5
+ try {
6
+ return JSON.parse(value);
7
+ }
8
+ catch (error) {
9
+ throw new Error(`Invalid JSON for --${flagName}: ${error?.message ?? 'parse failed'}`);
10
+ }
11
+ }
12
+ function parseFlexibleValue(value, flagName) {
13
+ if (value === undefined) {
14
+ return undefined;
15
+ }
16
+ const trimmed = value.trim();
17
+ if (!trimmed) {
18
+ return value;
19
+ }
20
+ if (trimmed.startsWith('[') || trimmed.startsWith('{') || trimmed === 'null' || trimmed === 'true' || trimmed === 'false') {
21
+ return parseJson(trimmed, flagName);
22
+ }
23
+ return value;
24
+ }
25
+ function parseObjectFlag(value, flagName) {
26
+ if (value === undefined) {
27
+ return undefined;
28
+ }
29
+ const parsed = parseJson(value, flagName);
30
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
31
+ throw new Error(`--${flagName} must be a JSON object`);
32
+ }
33
+ return parsed;
34
+ }
35
+ function parseJsonArrayFlag(value, flagName) {
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+ const parsed = parseJson(value, flagName);
40
+ if (!Array.isArray(parsed)) {
41
+ throw new Error(`--${flagName} must be a JSON array`);
42
+ }
43
+ return parsed;
44
+ }
45
+ function parseStringArrayFlags(value, flagName) {
46
+ if (value === undefined) {
47
+ return undefined;
48
+ }
49
+ if (Array.isArray(value)) {
50
+ if (value.length === 1) {
51
+ const trimmed = value[0].trim();
52
+ if (trimmed.startsWith('[')) {
53
+ const parsed = parseJson(trimmed, flagName);
54
+ if (!Array.isArray(parsed)) {
55
+ throw new Error(`--${flagName} must be repeated or use a JSON array`);
56
+ }
57
+ return parsed.map((item) => String(item));
58
+ }
59
+ }
60
+ return value.map((item) => String(item));
61
+ }
62
+ const trimmed = value.trim();
63
+ if (trimmed.startsWith('[')) {
64
+ const parsed = parseJson(trimmed, flagName);
65
+ if (!Array.isArray(parsed)) {
66
+ throw new Error(`--${flagName} must be repeated or use a JSON array`);
67
+ }
68
+ return parsed.map((item) => String(item));
69
+ }
70
+ return [String(value)];
71
+ }
72
+ function printResponse(command, response, jsonOutput) {
73
+ if (!response.ok) {
74
+ command.error(`Request failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`);
75
+ }
76
+ if (jsonOutput) {
77
+ command.log(JSON.stringify(response.data, null, 2));
78
+ return;
79
+ }
80
+ command.log(`HTTP ${response.status}`);
81
+ }
82
+ export const resourceBaseFlags = {
83
+ 'base-url': Flags.string({
84
+ description: 'NocoBase API base URL, for example http://localhost:13000/api',
85
+ }),
86
+ verbose: Flags.boolean({
87
+ description: 'Show detailed progress output',
88
+ default: false,
89
+ }),
90
+ env: Flags.string({
91
+ char: 'e',
92
+ description: 'Environment name',
93
+ }),
94
+ token: Flags.string({
95
+ char: 't',
96
+ description: 'Bearer token override',
97
+ }),
98
+ 'json-output': Flags.boolean({
99
+ char: 'j',
100
+ description: 'Print raw JSON response',
101
+ default: true,
102
+ allowNo: true,
103
+ }),
104
+ resource: Flags.string({
105
+ description: 'Resource name such as users, orders, or association resources like posts.comments',
106
+ required: true,
107
+ }),
108
+ 'data-source': Flags.string({
109
+ description: 'Data source key. Defaults to main.',
110
+ }),
111
+ };
112
+ export const resourceAssociationFlags = {
113
+ 'source-id': Flags.string({
114
+ description: 'Source record ID for association resources like posts.comments.',
115
+ }),
116
+ };
117
+ export const listFlags = {
118
+ ...resourceBaseFlags,
119
+ ...resourceAssociationFlags,
120
+ filter: Flags.string({
121
+ description: 'Filter object as JSON',
122
+ }),
123
+ fields: Flags.string({
124
+ description: 'Fields to query. Repeat the flag or pass a JSON array.',
125
+ multiple: true,
126
+ }),
127
+ appends: Flags.string({
128
+ description: 'Association or appended fields to include. Repeat the flag or pass a JSON array.',
129
+ multiple: true,
130
+ }),
131
+ except: Flags.string({
132
+ description: 'Fields to exclude from the result. Repeat the flag or pass a JSON array.',
133
+ multiple: true,
134
+ }),
135
+ sort: Flags.string({
136
+ description: 'Sort fields such as -createdAt. Repeat the flag or pass a JSON array.',
137
+ multiple: true,
138
+ }),
139
+ page: Flags.integer({
140
+ description: 'Page number for list action.',
141
+ }),
142
+ 'page-size': Flags.integer({
143
+ description: 'Page size for list action.',
144
+ }),
145
+ paginate: Flags.boolean({
146
+ description: 'Whether to use pagination for list action.',
147
+ allowNo: true,
148
+ }),
149
+ };
150
+ export const getFlags = {
151
+ ...resourceBaseFlags,
152
+ ...resourceAssociationFlags,
153
+ 'filter-by-tk': Flags.string({
154
+ description: 'Primary key value used by get. Supports JSON arrays for composite or multiple keys.',
155
+ }),
156
+ fields: Flags.string({
157
+ description: 'Fields to query. Repeat the flag or pass a JSON array.',
158
+ multiple: true,
159
+ }),
160
+ appends: Flags.string({
161
+ description: 'Association or appended fields to include. Repeat the flag or pass a JSON array.',
162
+ multiple: true,
163
+ }),
164
+ except: Flags.string({
165
+ description: 'Fields to exclude from the result. Repeat the flag or pass a JSON array.',
166
+ multiple: true,
167
+ }),
168
+ };
169
+ export const createFlags = {
170
+ ...resourceBaseFlags,
171
+ ...resourceAssociationFlags,
172
+ values: Flags.string({
173
+ description: 'Record values used by create as a JSON object.',
174
+ required: true,
175
+ }),
176
+ whitelist: Flags.string({
177
+ description: 'Fields allowed to be written. Repeat the flag or pass a JSON array.',
178
+ multiple: true,
179
+ }),
180
+ blacklist: Flags.string({
181
+ description: 'Fields forbidden to be written. Repeat the flag or pass a JSON array.',
182
+ multiple: true,
183
+ }),
184
+ };
185
+ export const updateFlags = {
186
+ ...resourceBaseFlags,
187
+ ...resourceAssociationFlags,
188
+ 'filter-by-tk': Flags.string({
189
+ description: 'Primary key value used by update. Supports JSON arrays for composite or multiple keys.',
190
+ }),
191
+ filter: Flags.string({
192
+ description: 'Filter object for update as JSON.',
193
+ }),
194
+ values: Flags.string({
195
+ description: 'Record values used by update as a JSON object.',
196
+ required: true,
197
+ }),
198
+ whitelist: Flags.string({
199
+ description: 'Fields allowed to be written. Repeat the flag or pass a JSON array.',
200
+ multiple: true,
201
+ }),
202
+ blacklist: Flags.string({
203
+ description: 'Fields forbidden to be written. Repeat the flag or pass a JSON array.',
204
+ multiple: true,
205
+ }),
206
+ 'update-association-values': Flags.string({
207
+ description: 'Association fields that should be updated together. Repeat the flag or pass a JSON array.',
208
+ multiple: true,
209
+ }),
210
+ 'force-update': Flags.boolean({
211
+ description: 'Whether update should force writing unchanged values.',
212
+ allowNo: true,
213
+ }),
214
+ };
215
+ export const destroyFlags = {
216
+ ...resourceBaseFlags,
217
+ ...resourceAssociationFlags,
218
+ 'filter-by-tk': Flags.string({
219
+ description: 'Primary key value used by destroy. Supports JSON arrays for composite or multiple keys.',
220
+ }),
221
+ filter: Flags.string({
222
+ description: 'Filter object for destroy as JSON.',
223
+ }),
224
+ };
225
+ export const queryFlags = {
226
+ ...resourceBaseFlags,
227
+ measures: Flags.string({
228
+ description: 'Measure definitions for query aggregation as a JSON array.',
229
+ }),
230
+ dimensions: Flags.string({
231
+ description: 'Dimension definitions for query aggregation as a JSON array.',
232
+ }),
233
+ orders: Flags.string({
234
+ description: 'Order definitions for query aggregation as a JSON array.',
235
+ }),
236
+ filter: Flags.string({
237
+ description: 'Filter object for query as JSON.',
238
+ }),
239
+ having: Flags.string({
240
+ description: 'Having object for grouped query as JSON.',
241
+ }),
242
+ limit: Flags.integer({
243
+ description: 'Limit for query result rows.',
244
+ }),
245
+ offset: Flags.integer({
246
+ description: 'Offset for query result rows.',
247
+ }),
248
+ timezone: Flags.string({
249
+ description: 'Optional timezone for query formatting.',
250
+ }),
251
+ };
252
+ function pickSharedArgs(flags) {
253
+ return {
254
+ resource: flags.resource,
255
+ dataSource: flags['data-source'],
256
+ sourceId: parseFlexibleValue(flags['source-id'], 'source-id'),
257
+ };
258
+ }
259
+ export function buildListArgs(flags) {
260
+ return {
261
+ ...pickSharedArgs(flags),
262
+ filter: parseObjectFlag(flags.filter, 'filter'),
263
+ fields: parseStringArrayFlags(flags.fields, 'fields'),
264
+ appends: parseStringArrayFlags(flags.appends, 'appends'),
265
+ except: parseStringArrayFlags(flags.except, 'except'),
266
+ sort: parseStringArrayFlags(flags.sort, 'sort'),
267
+ page: flags.page,
268
+ pageSize: flags['page-size'],
269
+ paginate: flags.paginate,
270
+ };
271
+ }
272
+ export function buildGetArgs(flags) {
273
+ return {
274
+ ...pickSharedArgs(flags),
275
+ filterByTk: parseFlexibleValue(flags['filter-by-tk'], 'filter-by-tk'),
276
+ fields: parseStringArrayFlags(flags.fields, 'fields'),
277
+ appends: parseStringArrayFlags(flags.appends, 'appends'),
278
+ except: parseStringArrayFlags(flags.except, 'except'),
279
+ };
280
+ }
281
+ export function buildCreateArgs(flags) {
282
+ return {
283
+ ...pickSharedArgs(flags),
284
+ values: parseObjectFlag(flags.values, 'values'),
285
+ whitelist: parseStringArrayFlags(flags.whitelist, 'whitelist'),
286
+ blacklist: parseStringArrayFlags(flags.blacklist, 'blacklist'),
287
+ };
288
+ }
289
+ export function buildUpdateArgs(flags) {
290
+ return {
291
+ ...pickSharedArgs(flags),
292
+ filterByTk: parseFlexibleValue(flags['filter-by-tk'], 'filter-by-tk'),
293
+ filter: parseObjectFlag(flags.filter, 'filter'),
294
+ values: parseObjectFlag(flags.values, 'values'),
295
+ whitelist: parseStringArrayFlags(flags.whitelist, 'whitelist'),
296
+ blacklist: parseStringArrayFlags(flags.blacklist, 'blacklist'),
297
+ updateAssociationValues: parseStringArrayFlags(flags['update-association-values'], 'update-association-values'),
298
+ forceUpdate: flags['force-update'],
299
+ };
300
+ }
301
+ export function buildDestroyArgs(flags) {
302
+ return {
303
+ ...pickSharedArgs(flags),
304
+ filterByTk: parseFlexibleValue(flags['filter-by-tk'], 'filter-by-tk'),
305
+ filter: parseObjectFlag(flags.filter, 'filter'),
306
+ };
307
+ }
308
+ export function buildQueryArgs(flags) {
309
+ return {
310
+ ...pickSharedArgs(flags),
311
+ measures: parseJsonArrayFlag(flags.measures, 'measures'),
312
+ dimensions: parseJsonArrayFlag(flags.dimensions, 'dimensions'),
313
+ orders: parseJsonArrayFlag(flags.orders, 'orders'),
314
+ filter: parseObjectFlag(flags.filter, 'filter'),
315
+ having: parseObjectFlag(flags.having, 'having'),
316
+ limit: flags.limit,
317
+ offset: flags.offset,
318
+ timezone: flags.timezone,
319
+ };
320
+ }
321
+ export async function runResourceCommand(command, action, flags, args) {
322
+ setVerboseMode(Boolean(flags.verbose));
323
+ const response = await executeResourceRequest({
324
+ envName: flags.env,
325
+ baseUrl: flags['base-url'],
326
+ token: flags.token,
327
+ action,
328
+ args,
329
+ });
330
+ printResponse(command, response, flags['json-output']);
331
+ }
@@ -0,0 +1,103 @@
1
+ import { executeRawApiRequest } from "./api-client.js";
2
+ function buildActionUrl(resource, action, sourceId) {
3
+ if (typeof sourceId === 'undefined' || sourceId === null || !resource.includes('.')) {
4
+ return `${resource}:${action}`;
5
+ }
6
+ const [parentResource, childResource] = resource.split('.');
7
+ return `${parentResource}/${encodeURIComponent(String(sourceId))}/${childResource}:${action}`;
8
+ }
9
+ function buildQueryValue(value) {
10
+ if (typeof value === 'undefined') {
11
+ return undefined;
12
+ }
13
+ if (value === null) {
14
+ return null;
15
+ }
16
+ if (Array.isArray(value)) {
17
+ return value;
18
+ }
19
+ if (typeof value === 'object') {
20
+ return JSON.stringify(value);
21
+ }
22
+ return value;
23
+ }
24
+ function buildRequestQuery(args) {
25
+ const query = {
26
+ filterByTk: buildQueryValue(args.filterByTk),
27
+ filter: buildQueryValue(args.filter),
28
+ fields: buildQueryValue(args.fields),
29
+ appends: buildQueryValue(args.appends),
30
+ except: buildQueryValue(args.except),
31
+ sort: buildQueryValue(args.sort),
32
+ page: buildQueryValue(args.page),
33
+ pageSize: buildQueryValue(args.pageSize),
34
+ paginate: buildQueryValue(args.paginate),
35
+ whitelist: buildQueryValue(args.whitelist),
36
+ blacklist: buildQueryValue(args.blacklist),
37
+ updateAssociationValues: buildQueryValue(args.updateAssociationValues),
38
+ forceUpdate: buildQueryValue(args.forceUpdate),
39
+ };
40
+ for (const key of Object.keys(query)) {
41
+ if (typeof query[key] === 'undefined') {
42
+ delete query[key];
43
+ }
44
+ }
45
+ return query;
46
+ }
47
+ function buildQueryPayload(args) {
48
+ const payload = {
49
+ measures: args.measures,
50
+ dimensions: args.dimensions,
51
+ orders: args.orders,
52
+ filter: args.filter,
53
+ having: args.having,
54
+ limit: args.limit,
55
+ offset: args.offset,
56
+ };
57
+ for (const key of Object.keys(payload)) {
58
+ if (typeof payload[key] === 'undefined') {
59
+ delete payload[key];
60
+ }
61
+ }
62
+ return payload;
63
+ }
64
+ function buildCrudPayload(action, args) {
65
+ if ((action === 'create' || action === 'update') && args.values) {
66
+ return args.values;
67
+ }
68
+ }
69
+ function buildActionQuery(action, args) {
70
+ if (action === 'query') {
71
+ return undefined;
72
+ }
73
+ return buildRequestQuery(args);
74
+ }
75
+ function buildActionPayload(action, args) {
76
+ if (action === 'query') {
77
+ return buildQueryPayload(args);
78
+ }
79
+ return buildCrudPayload(action, args);
80
+ }
81
+ function buildHeaders(action, args) {
82
+ const headers = {};
83
+ if (args.dataSource && args.dataSource !== 'main') {
84
+ headers['x-data-source'] = args.dataSource;
85
+ }
86
+ if (action === 'query' && args.timezone) {
87
+ headers['x-timezone'] = args.timezone;
88
+ }
89
+ return headers;
90
+ }
91
+ export async function executeResourceRequest(options) {
92
+ const path = `/${buildActionUrl(options.args.resource, options.action, options.args.sourceId)}`;
93
+ return executeRawApiRequest({
94
+ envName: options.envName,
95
+ baseUrl: options.baseUrl,
96
+ token: options.token,
97
+ method: 'POST',
98
+ path,
99
+ query: buildActionQuery(options.action, options.args),
100
+ body: buildActionPayload(options.action, options.args),
101
+ headers: buildHeaders(options.action, options.args),
102
+ });
103
+ }