@memberjunction/database-designer-actions 0.0.1 → 5.32.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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +37 -0
- package/dist/actions/base-database-designer.action.d.ts +52 -0
- package/dist/actions/base-database-designer.action.d.ts.map +1 -0
- package/dist/actions/base-database-designer.action.js +162 -0
- package/dist/actions/base-database-designer.action.js.map +1 -0
- package/dist/actions/create-entity.action.d.ts +36 -0
- package/dist/actions/create-entity.action.d.ts.map +1 -0
- package/dist/actions/create-entity.action.js +65 -0
- package/dist/actions/create-entity.action.js.map +1 -0
- package/dist/actions/describe-entity.action.d.ts +29 -0
- package/dist/actions/describe-entity.action.d.ts.map +1 -0
- package/dist/actions/describe-entity.action.js +116 -0
- package/dist/actions/describe-entity.action.js.map +1 -0
- package/dist/actions/list-entities.action.d.ts +26 -0
- package/dist/actions/list-entities.action.d.ts.map +1 -0
- package/dist/actions/list-entities.action.js +91 -0
- package/dist/actions/list-entities.action.js.map +1 -0
- package/dist/actions/modify-entity.action.d.ts +28 -0
- package/dist/actions/modify-entity.action.d.ts.map +1 -0
- package/dist/actions/modify-entity.action.js +65 -0
- package/dist/actions/modify-entity.action.js.map +1 -0
- package/dist/actions/validate-entity-schema.action.d.ts +38 -0
- package/dist/actions/validate-entity-schema.action.d.ts.map +1 -0
- package/dist/actions/validate-entity-schema.action.js +85 -0
- package/dist/actions/validate-entity-schema.action.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -7
- package/src/__tests__/validate-entity-schema.action.test.ts +243 -0
- package/src/actions/base-database-designer.action.ts +210 -0
- package/src/actions/create-entity.action.ts +66 -0
- package/src/actions/describe-entity.action.ts +160 -0
- package/src/actions/list-entities.action.ts +110 -0
- package/src/actions/modify-entity.action.ts +75 -0
- package/src/actions/validate-entity-schema.action.ts +89 -0
- package/src/index.ts +15 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
- package/README.md +0 -45
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import type { RunActionParams } from '@memberjunction/actions-base';
|
|
3
|
+
|
|
4
|
+
// ─── Hoisted mock state ──────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const mockState = vi.hoisted(() => ({
|
|
7
|
+
authorizations: [] as { Name: string; UserCanExecute: () => boolean }[],
|
|
8
|
+
validationResult: {
|
|
9
|
+
Valid: true,
|
|
10
|
+
Errors: [] as string[],
|
|
11
|
+
Warnings: [] as string[],
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// ─── Mock all external dependencies ─────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
vi.mock('@memberjunction/core', () => ({
|
|
18
|
+
Metadata: vi.fn().mockImplementation(function(this: unknown) {
|
|
19
|
+
return { Authorizations: mockState.authorizations };
|
|
20
|
+
}),
|
|
21
|
+
AuthorizationEvaluator: vi.fn().mockImplementation(function(this: unknown) {
|
|
22
|
+
return {
|
|
23
|
+
UserCanExecuteWithAncestors: vi.fn().mockImplementation(
|
|
24
|
+
(auth: { UserCanExecute?: () => boolean }) => auth?.UserCanExecute?.() ?? false
|
|
25
|
+
),
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
LogError: vi.fn(),
|
|
29
|
+
UserInfo: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@memberjunction/database-designer-core', () => ({
|
|
33
|
+
DatabaseSchemaValidationService: vi.fn().mockImplementation(function(this: unknown) {
|
|
34
|
+
return {
|
|
35
|
+
validate: vi.fn().mockImplementation(async () => mockState.validationResult),
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
AUTHORIZATIONS: {
|
|
39
|
+
CREATE_IN_UDT_SCHEMA: 'Create in UDT Schema',
|
|
40
|
+
CREATE_IN_CUSTOM_SCHEMA: 'Create in Custom Schema',
|
|
41
|
+
MODIFY_OWN_ENTITIES: 'Modify Own Entities',
|
|
42
|
+
MODIFY_ANY_UDT_ENTITIES: 'Modify Any UDT Entities',
|
|
43
|
+
},
|
|
44
|
+
UDT_SCHEMA_NAME: '__mj_UDT',
|
|
45
|
+
DatabaseDesignerPipelineExecutor: {},
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock('@memberjunction/global', () => ({
|
|
49
|
+
RegisterClass: () => (_target: unknown) => _target,
|
|
50
|
+
LogError: vi.fn(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
vi.mock('@memberjunction/actions', () => ({
|
|
54
|
+
BaseAction: class BaseAction {},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock('@memberjunction/schema-engine', () => ({}));
|
|
58
|
+
|
|
59
|
+
// ─── Import after mocks ──────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
import { ValidateEntitySchemaAction } from '../actions/validate-entity-schema.action.js';
|
|
62
|
+
import { AUTHORIZATIONS } from '@memberjunction/database-designer-core';
|
|
63
|
+
|
|
64
|
+
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const CONTEXT_USER = { ID: 'user-1', Email: 'test@example.com' } as ReturnType<typeof vi.fn>;
|
|
67
|
+
|
|
68
|
+
const VALID_TABLE_DEFINITION = {
|
|
69
|
+
SchemaName: '__mj_UDT',
|
|
70
|
+
TableName: 'ProjectMilestones',
|
|
71
|
+
EntityName: 'Project Milestones',
|
|
72
|
+
Description: 'Milestones for projects',
|
|
73
|
+
Columns: [{ Name: 'Title', Type: 'string', IsNullable: false }],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function makeParams(overrides: {
|
|
77
|
+
tableDefinition?: unknown;
|
|
78
|
+
modificationType?: string;
|
|
79
|
+
contextUser?: unknown;
|
|
80
|
+
} = {}): RunActionParams {
|
|
81
|
+
const params: { Name: string; Type: string; Value: unknown }[] = [];
|
|
82
|
+
|
|
83
|
+
const td = overrides.tableDefinition !== undefined
|
|
84
|
+
? overrides.tableDefinition
|
|
85
|
+
: VALID_TABLE_DEFINITION;
|
|
86
|
+
if (td !== null) {
|
|
87
|
+
params.push({ Name: 'TableDefinition', Type: 'Input', Value: td });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (overrides.modificationType) {
|
|
91
|
+
params.push({ Name: 'ModificationType', Type: 'Input', Value: overrides.modificationType });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
Params: params,
|
|
96
|
+
ContextUser: (overrides.contextUser ?? CONTEXT_USER) as ReturnType<typeof vi.fn>,
|
|
97
|
+
} as unknown as RunActionParams;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Access protected InternalRunAction for testing (standard MJ action test pattern)
|
|
101
|
+
type TestableAction = {
|
|
102
|
+
InternalRunAction(params: RunActionParams): Promise<import('@memberjunction/actions-base').ActionResultSimple>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function makeAction(): TestableAction {
|
|
106
|
+
return new ValidateEntitySchemaAction() as unknown as TestableAction;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const AUTH_CREATE_UDT = { Name: AUTHORIZATIONS.CREATE_IN_UDT_SCHEMA, UserCanExecute: () => true };
|
|
110
|
+
|
|
111
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
describe('ValidateEntitySchemaAction', () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
mockState.authorizations = [];
|
|
116
|
+
mockState.validationResult = { Valid: true, Errors: [], Warnings: [] };
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('parameter validation', () => {
|
|
120
|
+
it('returns MISSING_PARAMETER when TableDefinition is absent', async () => {
|
|
121
|
+
const action = makeAction();
|
|
122
|
+
const result = await action.InternalRunAction(makeParams({ tableDefinition: null }));
|
|
123
|
+
|
|
124
|
+
expect(result.Success).toBe(false);
|
|
125
|
+
expect(result.ResultCode).toBe('MISSING_PARAMETER');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns INVALID_TABLE_DEFINITION when TableDefinition is not a valid object', async () => {
|
|
129
|
+
const action = makeAction();
|
|
130
|
+
const result = await action.InternalRunAction(makeParams({ tableDefinition: '{bad json}' }));
|
|
131
|
+
|
|
132
|
+
expect(result.Success).toBe(false);
|
|
133
|
+
expect(result.ResultCode).toBe('INVALID_TABLE_DEFINITION');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('authorization', () => {
|
|
138
|
+
it('returns UNAUTHORIZED when contextUser lacks the required auth', async () => {
|
|
139
|
+
// authorizations stays [] — no auth configured → access denied
|
|
140
|
+
const action = makeAction();
|
|
141
|
+
const result = await action.InternalRunAction(makeParams());
|
|
142
|
+
|
|
143
|
+
expect(result.Success).toBe(false);
|
|
144
|
+
expect(result.ResultCode).toBe('UNAUTHORIZED');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('proceeds to validation when user holds the required authorization', async () => {
|
|
148
|
+
mockState.authorizations = [AUTH_CREATE_UDT];
|
|
149
|
+
const action = makeAction();
|
|
150
|
+
const result = await action.InternalRunAction(makeParams());
|
|
151
|
+
|
|
152
|
+
// Auth passed — result code is from validation, not auth
|
|
153
|
+
expect(result.ResultCode).not.toBe('UNAUTHORIZED');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('validation outcomes', () => {
|
|
158
|
+
it('returns VALIDATION_PASSED (Success=true) when the definition is valid', async () => {
|
|
159
|
+
mockState.authorizations = [AUTH_CREATE_UDT];
|
|
160
|
+
mockState.validationResult = { Valid: true, Errors: [], Warnings: [] };
|
|
161
|
+
|
|
162
|
+
const action = makeAction();
|
|
163
|
+
const result = await action.InternalRunAction(makeParams());
|
|
164
|
+
|
|
165
|
+
expect(result.Success).toBe(true);
|
|
166
|
+
expect(result.ResultCode).toBe('VALIDATION_PASSED');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('returns VALIDATION_FAILED (Success=true) when the definition has errors', async () => {
|
|
170
|
+
mockState.authorizations = [AUTH_CREATE_UDT];
|
|
171
|
+
mockState.validationResult = {
|
|
172
|
+
Valid: false,
|
|
173
|
+
Errors: ['Schema dbo is protected'],
|
|
174
|
+
Warnings: [],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const action = makeAction();
|
|
178
|
+
const result = await action.InternalRunAction(makeParams());
|
|
179
|
+
|
|
180
|
+
// Success=true because the action itself succeeded (validation is the output)
|
|
181
|
+
expect(result.Success).toBe(true);
|
|
182
|
+
expect(result.ResultCode).toBe('VALIDATION_FAILED');
|
|
183
|
+
expect(result.Message).toContain('Schema dbo is protected');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('writes ValidationResult as an output param', async () => {
|
|
187
|
+
mockState.authorizations = [AUTH_CREATE_UDT];
|
|
188
|
+
mockState.validationResult = { Valid: true, Errors: [], Warnings: ['Minor advisory'] };
|
|
189
|
+
|
|
190
|
+
const params = makeParams();
|
|
191
|
+
const action = makeAction();
|
|
192
|
+
await action.InternalRunAction(params);
|
|
193
|
+
|
|
194
|
+
const outputParam = params.Params.find(p => p.Name === 'ValidationResult');
|
|
195
|
+
expect(outputParam).toBeDefined();
|
|
196
|
+
expect(outputParam?.Value).toMatchObject({
|
|
197
|
+
Valid: true,
|
|
198
|
+
Warnings: ['Minor advisory'],
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('writes ValidationResult output even when validation fails', async () => {
|
|
203
|
+
mockState.authorizations = [AUTH_CREATE_UDT];
|
|
204
|
+
mockState.validationResult = {
|
|
205
|
+
Valid: false,
|
|
206
|
+
Errors: ['Reserved column ID'],
|
|
207
|
+
Warnings: [],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const params = makeParams();
|
|
211
|
+
const action = makeAction();
|
|
212
|
+
await action.InternalRunAction(params);
|
|
213
|
+
|
|
214
|
+
const outputParam = params.Params.find(p => p.Name === 'ValidationResult');
|
|
215
|
+
expect(outputParam?.Value).toMatchObject({
|
|
216
|
+
Valid: false,
|
|
217
|
+
Errors: ['Reserved column ID'],
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('ModificationType routing', () => {
|
|
223
|
+
it('defaults to create modification type when ModificationType is absent', async () => {
|
|
224
|
+
// __mj_UDT schema → CREATE_IN_UDT_SCHEMA auth required
|
|
225
|
+
mockState.authorizations = [AUTH_CREATE_UDT];
|
|
226
|
+
const action = makeAction();
|
|
227
|
+
const result = await action.InternalRunAction(makeParams());
|
|
228
|
+
|
|
229
|
+
expect(result.ResultCode).toBe('VALIDATION_PASSED');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('uses MODIFY_OWN_ENTITIES auth when ModificationType is alter', async () => {
|
|
233
|
+
mockState.authorizations = [{
|
|
234
|
+
Name: AUTHORIZATIONS.MODIFY_OWN_ENTITIES,
|
|
235
|
+
UserCanExecute: () => true,
|
|
236
|
+
}];
|
|
237
|
+
const action = makeAction();
|
|
238
|
+
const result = await action.InternalRunAction(makeParams({ modificationType: 'alter' }));
|
|
239
|
+
|
|
240
|
+
expect(result.ResultCode).not.toBe('UNAUTHORIZED');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module base-database-designer.action
|
|
3
|
+
* @description Abstract base for all Database Designer actions.
|
|
4
|
+
*
|
|
5
|
+
* Provides common parameter extraction, authorization checking, and result
|
|
6
|
+
* building so that each concrete action stays thin and focused. All the
|
|
7
|
+
* shared logic lives here; concrete actions only implement `InternalRunAction`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BaseAction } from '@memberjunction/actions';
|
|
11
|
+
import type { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
12
|
+
import { AuthorizationEvaluator, Metadata, UserInfo, LogError } from '@memberjunction/core';
|
|
13
|
+
import type { TableDefinition } from '@memberjunction/schema-engine';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
DatabaseDesignerPipelineExecutor,
|
|
17
|
+
type PipelineExecutionOptions,
|
|
18
|
+
AUTHORIZATIONS,
|
|
19
|
+
UDT_SCHEMA_NAME,
|
|
20
|
+
type PipelineExecutionResult,
|
|
21
|
+
} from '@memberjunction/database-designer-core';
|
|
22
|
+
|
|
23
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export abstract class BaseDatabaseDesignerAction extends BaseAction {
|
|
26
|
+
|
|
27
|
+
// ─── Param extraction ────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract and parse the `TableDefinition` param (JSON object or string).
|
|
31
|
+
* Returns `{ error }` on missing/malformed input, `{ td }` on success.
|
|
32
|
+
*/
|
|
33
|
+
protected getTableDefinitionParam(
|
|
34
|
+
params: RunActionParams
|
|
35
|
+
): { td?: TableDefinition; error?: ActionResultSimple } {
|
|
36
|
+
const param = this.findParam(params, 'TableDefinition');
|
|
37
|
+
if (!param?.Value) {
|
|
38
|
+
return {
|
|
39
|
+
error: this.missing('TableDefinition', 'A complete TableDefinition object is required.'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const raw = param.Value;
|
|
44
|
+
const td = typeof raw === 'string' ? this.tryParse(raw) : raw as TableDefinition;
|
|
45
|
+
if (!td || typeof td !== 'object') {
|
|
46
|
+
return {
|
|
47
|
+
error: {
|
|
48
|
+
Success: false,
|
|
49
|
+
ResultCode: 'INVALID_TABLE_DEFINITION',
|
|
50
|
+
Message: 'TableDefinition must be a valid JSON object.',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { td: td as TableDefinition };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Extract the `ModificationType` param ('create' | 'alter'). Defaults to 'create'. */
|
|
59
|
+
protected getModificationTypeParam(params: RunActionParams): 'create' | 'alter' {
|
|
60
|
+
const v = this.findParam(params, 'ModificationType')?.Value;
|
|
61
|
+
return v === 'alter' ? 'alter' : 'create';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Extract a boolean param by name with a fallback default. */
|
|
65
|
+
protected getBoolParam(params: RunActionParams, name: string, defaultValue: boolean): boolean {
|
|
66
|
+
const v = this.findParam(params, name)?.Value;
|
|
67
|
+
if (v === undefined || v === null) return defaultValue;
|
|
68
|
+
const s = String(v).trim().toLowerCase();
|
|
69
|
+
if (s === 'true' || s === '1' || s === 'yes') return true;
|
|
70
|
+
if (s === 'false' || s === '0' || s === 'no') return false;
|
|
71
|
+
return defaultValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Extract a string param by name, returning undefined when absent. */
|
|
75
|
+
protected getStringParam(params: RunActionParams, name: string): string | undefined {
|
|
76
|
+
const v = this.findParam(params, name)?.Value;
|
|
77
|
+
if (v === undefined || v === null) return undefined;
|
|
78
|
+
const s = String(v).trim();
|
|
79
|
+
return s.length > 0 ? s : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Append an output param to the params array so callers can read results. */
|
|
83
|
+
protected addOutputParam(params: RunActionParams, name: string, value: unknown): void {
|
|
84
|
+
params.Params.push({ Name: name, Type: 'Output', Value: value });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Authorization ────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check that the contextUser holds the authorization required for the
|
|
91
|
+
* requested operation type. Returns an error string on failure, null on success.
|
|
92
|
+
*/
|
|
93
|
+
protected async checkAuthorization(
|
|
94
|
+
tableDefinition: TableDefinition,
|
|
95
|
+
modificationType: 'create' | 'alter',
|
|
96
|
+
contextUser: UserInfo
|
|
97
|
+
): Promise<string | null> {
|
|
98
|
+
const authName = this.resolveAuthorizationName(tableDefinition, modificationType);
|
|
99
|
+
const md = new Metadata(); // global-provider-ok: MJAPI server-side, single-provider deployment
|
|
100
|
+
const auth = md.Authorizations.find(a => a.Name === authName);
|
|
101
|
+
|
|
102
|
+
if (!auth) {
|
|
103
|
+
return `Authorization '${authName}' is not configured in this system.`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const evaluator = new AuthorizationEvaluator();
|
|
107
|
+
if (!evaluator.UserCanExecuteWithAncestors(auth, contextUser, md.Authorizations)) {
|
|
108
|
+
return `User does not have the '${authName}' authorization required for this operation.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Map operation to its minimum required authorization name. */
|
|
115
|
+
private resolveAuthorizationName(
|
|
116
|
+
tableDefinition: TableDefinition,
|
|
117
|
+
modificationType: 'create' | 'alter'
|
|
118
|
+
): string {
|
|
119
|
+
if (modificationType === 'alter') {
|
|
120
|
+
return AUTHORIZATIONS.MODIFY_OWN_ENTITIES;
|
|
121
|
+
}
|
|
122
|
+
return tableDefinition.SchemaName.toLowerCase() === UDT_SCHEMA_NAME.toLowerCase()
|
|
123
|
+
? AUTHORIZATIONS.CREATE_IN_UDT_SCHEMA
|
|
124
|
+
: AUTHORIZATIONS.CREATE_IN_CUSTOM_SCHEMA;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Pipeline options ─────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/** Build PipelineExecutionOptions from action params. */
|
|
130
|
+
protected buildPipelineOptions(
|
|
131
|
+
params: RunActionParams,
|
|
132
|
+
source: string
|
|
133
|
+
): PipelineExecutionOptions {
|
|
134
|
+
return {
|
|
135
|
+
SkipGitCommit: this.getBoolParam(params, 'SkipGitCommit', false),
|
|
136
|
+
SkipRestart: this.getBoolParam(params, 'SkipRestart', false),
|
|
137
|
+
Source: source,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Result building ─────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Map a PipelineExecutionResult to ActionResultSimple and populate output
|
|
145
|
+
* params so workflow engines can read EntityID, EntityName, etc.
|
|
146
|
+
*/
|
|
147
|
+
protected buildPipelineResult(
|
|
148
|
+
execResult: PipelineExecutionResult,
|
|
149
|
+
params: RunActionParams
|
|
150
|
+
): ActionResultSimple {
|
|
151
|
+
if (execResult.Success) {
|
|
152
|
+
this.addOutputParam(params, 'EntityID', execResult.EntityID ?? null);
|
|
153
|
+
this.addOutputParam(params, 'EntityName', execResult.EntityName ?? null);
|
|
154
|
+
this.addOutputParam(params, 'SchemaName', execResult.SchemaName ?? null);
|
|
155
|
+
this.addOutputParam(params, 'TableName', execResult.TableName ?? null);
|
|
156
|
+
this.addOutputParam(params, 'PipelineSteps', execResult.PipelineSteps ?? []);
|
|
157
|
+
return {
|
|
158
|
+
Success: true,
|
|
159
|
+
ResultCode: 'SUCCESS',
|
|
160
|
+
Message: `Entity '${execResult.EntityName ?? execResult.TableName}' created/modified successfully.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.addOutputParam(params, 'PipelineSteps', execResult.PipelineSteps ?? []);
|
|
165
|
+
return {
|
|
166
|
+
Success: false,
|
|
167
|
+
ResultCode: 'PIPELINE_FAILED',
|
|
168
|
+
Message: execResult.ErrorMessage ?? 'Pipeline execution failed without an error message.',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Common error handler for unexpected exceptions. */
|
|
173
|
+
protected handleUnexpected(error: unknown, operation: string): ActionResultSimple {
|
|
174
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
175
|
+
LogError(`DatabaseDesigner action '${operation}' threw: ${msg}`);
|
|
176
|
+
return {
|
|
177
|
+
Success: false,
|
|
178
|
+
ResultCode: 'UNEXPECTED_ERROR',
|
|
179
|
+
Message: `Unexpected error during ${operation}: ${msg}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Private helpers ──────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
private findParam(params: RunActionParams, name: string) {
|
|
186
|
+
return params.Params.find(
|
|
187
|
+
p => p.Name.trim().toLowerCase() === name.toLowerCase()
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private tryParse(raw: string): unknown | null {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(raw);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private missing(paramName: string, hint: string): ActionResultSimple {
|
|
200
|
+
return {
|
|
201
|
+
Success: false,
|
|
202
|
+
ResultCode: 'MISSING_PARAMETER',
|
|
203
|
+
Message: `Parameter '${paramName}' is required. ${hint}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Re-export the shared executor so callers that import from this module can
|
|
209
|
+
// access it without an extra import — actions are the primary consumers.
|
|
210
|
+
export { DatabaseDesignerPipelineExecutor };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module create-entity.action
|
|
3
|
+
* @description Action that creates a new MemberJunction entity from a
|
|
4
|
+
* TableDefinition. Validates authorization then delegates to the shared RSU
|
|
5
|
+
* pipeline executor.
|
|
6
|
+
*
|
|
7
|
+
* Intended callers:
|
|
8
|
+
* - Agent Manager's Planning Designer sub-agent
|
|
9
|
+
* - Any agent with the 'Create in UDT Schema' or 'Create in Custom Schema' auth
|
|
10
|
+
* - Workflow engines / low-code builders
|
|
11
|
+
*
|
|
12
|
+
* This action does NOT run the full conversational Database Designer flow.
|
|
13
|
+
* Callers must supply a pre-formed TableDefinition.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
17
|
+
import { BaseAction } from '@memberjunction/actions';
|
|
18
|
+
import type { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
19
|
+
|
|
20
|
+
import { DatabaseDesignerPipelineExecutor, UDT_SETTINGS } from '@memberjunction/database-designer-core';
|
|
21
|
+
|
|
22
|
+
import { BaseDatabaseDesignerAction } from './base-database-designer.action.js';
|
|
23
|
+
|
|
24
|
+
// ─── Registration ─────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new MemberJunction entity from a fully-formed `TableDefinition`.
|
|
28
|
+
*
|
|
29
|
+
* Input params:
|
|
30
|
+
* - `TableDefinition` (required) — JSON object conforming to the schema-engine
|
|
31
|
+
* `TableDefinition` interface.
|
|
32
|
+
* - `SkipGitCommit` (optional, default false) — skip creating a migration git commit.
|
|
33
|
+
* - `SkipRestart` (optional, default false) — skip restarting MJAPI after the pipeline.
|
|
34
|
+
*
|
|
35
|
+
* Output params written on success:
|
|
36
|
+
* - `EntityID` — MJ entity UUID.
|
|
37
|
+
* - `EntityName` — Human-readable entity name.
|
|
38
|
+
* - `SchemaName` — Database schema.
|
|
39
|
+
* - `TableName` — Physical table name.
|
|
40
|
+
* - `PipelineSteps` — Array of step summaries from RuntimeSchemaManager.
|
|
41
|
+
*/
|
|
42
|
+
@RegisterClass(BaseAction, 'Create Entity')
|
|
43
|
+
export class CreateEntityAction extends BaseDatabaseDesignerAction {
|
|
44
|
+
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
|
|
45
|
+
try {
|
|
46
|
+
const { td, error: tdError } = this.getTableDefinitionParam(params);
|
|
47
|
+
if (tdError) return tdError;
|
|
48
|
+
|
|
49
|
+
const authError = await this.checkAuthorization(td!, 'create', params.ContextUser);
|
|
50
|
+
if (authError) {
|
|
51
|
+
return { Success: false, ResultCode: 'UNAUTHORIZED', Message: authError };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const options = this.buildPipelineOptions(params, UDT_SETTINGS.SOURCE_DATABASE_DESIGNER);
|
|
55
|
+
const execResult = await DatabaseDesignerPipelineExecutor.CreateEntity(
|
|
56
|
+
td!,
|
|
57
|
+
params.ContextUser,
|
|
58
|
+
options
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return this.buildPipelineResult(execResult, params);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return this.handleUnexpected(err, 'Create Entity');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module describe-entity.action
|
|
3
|
+
* @description Returns detailed column-level metadata for a given entity.
|
|
4
|
+
*
|
|
5
|
+
* Useful for agents that need to inspect an existing entity before deciding
|
|
6
|
+
* whether to modify it, or for display in the Database Designer UI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { RegisterClass, UUIDsEqual } from '@memberjunction/global';
|
|
10
|
+
import { BaseAction } from '@memberjunction/actions';
|
|
11
|
+
import type { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
12
|
+
import { RunView, Metadata } from '@memberjunction/core';
|
|
13
|
+
|
|
14
|
+
import { escapeSqlLiteral } from '@memberjunction/database-designer-core';
|
|
15
|
+
|
|
16
|
+
import { BaseDatabaseDesignerAction } from './base-database-designer.action.js';
|
|
17
|
+
|
|
18
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
interface ColumnInfo {
|
|
21
|
+
Name: string;
|
|
22
|
+
Type: string;
|
|
23
|
+
IsNullable: boolean;
|
|
24
|
+
MaxLength: number | null;
|
|
25
|
+
Description: string | null;
|
|
26
|
+
IsRequired: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface EntityInfo {
|
|
30
|
+
EntityID: string;
|
|
31
|
+
EntityName: string;
|
|
32
|
+
SchemaName: string;
|
|
33
|
+
TableName: string;
|
|
34
|
+
Description: string | null;
|
|
35
|
+
Columns: ColumnInfo[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Registration ─────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Describes a MemberJunction entity including all its columns.
|
|
42
|
+
*
|
|
43
|
+
* Input params — supply exactly one identifier:
|
|
44
|
+
* - `EntityID` — The entity UUID.
|
|
45
|
+
* - `EntityName` — The human-readable entity name (e.g. "Project Milestones").
|
|
46
|
+
* - `TableName` — The physical table name. Optionally combine with
|
|
47
|
+
* `SchemaName` to narrow by schema.
|
|
48
|
+
*
|
|
49
|
+
* Output params:
|
|
50
|
+
* - `EntityInfo` — Full EntityInfo object (see above) or null if not found.
|
|
51
|
+
*/
|
|
52
|
+
@RegisterClass(BaseAction, 'Describe Entity')
|
|
53
|
+
export class DescribeEntityAction extends BaseDatabaseDesignerAction {
|
|
54
|
+
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
|
|
55
|
+
try {
|
|
56
|
+
const entityID = this.getStringParam(params, 'EntityID');
|
|
57
|
+
const entityName = this.getStringParam(params, 'EntityName');
|
|
58
|
+
const tableName = this.getStringParam(params, 'TableName');
|
|
59
|
+
const schemaName = this.getStringParam(params, 'SchemaName');
|
|
60
|
+
|
|
61
|
+
if (!entityID && !entityName && !tableName) {
|
|
62
|
+
return {
|
|
63
|
+
Success: false,
|
|
64
|
+
ResultCode: 'MISSING_PARAMETER',
|
|
65
|
+
Message: 'Provide at least one of: EntityID, EntityName, or TableName.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const entityInfo = await this.findEntity(entityID, entityName, tableName, schemaName, params);
|
|
70
|
+
|
|
71
|
+
if (!entityInfo) {
|
|
72
|
+
this.addOutputParam(params, 'EntityInfo', null);
|
|
73
|
+
return {
|
|
74
|
+
Success: false,
|
|
75
|
+
ResultCode: 'ENTITY_NOT_FOUND',
|
|
76
|
+
Message: 'No entity was found matching the provided identifiers.',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.addOutputParam(params, 'EntityInfo', entityInfo);
|
|
81
|
+
return {
|
|
82
|
+
Success: true,
|
|
83
|
+
ResultCode: 'SUCCESS',
|
|
84
|
+
Message: `Entity '${entityInfo.EntityName}' found with ${entityInfo.Columns.length} column(s).`,
|
|
85
|
+
};
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return this.handleUnexpected(err, 'Describe Entity');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Private helpers ──────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Resolve the entity using whichever identifier the caller supplied. */
|
|
94
|
+
private async findEntity(
|
|
95
|
+
entityID: string | undefined,
|
|
96
|
+
entityName: string | undefined,
|
|
97
|
+
tableName: string | undefined,
|
|
98
|
+
schemaName: string | undefined,
|
|
99
|
+
params: RunActionParams
|
|
100
|
+
): Promise<EntityInfo | null> {
|
|
101
|
+
const md = new Metadata(); // global-provider-ok: MJAPI server-side, single-provider deployment
|
|
102
|
+
|
|
103
|
+
// Fast path: look up by ID or name in the in-memory metadata cache.
|
|
104
|
+
let entity = entityID
|
|
105
|
+
? md.Entities.find(e => UUIDsEqual(e.ID, entityID))
|
|
106
|
+
: entityName
|
|
107
|
+
? md.Entities.find(e => e.Name.toLowerCase() === entityName.toLowerCase())
|
|
108
|
+
: md.Entities.find(
|
|
109
|
+
e =>
|
|
110
|
+
e.BaseTable.toLowerCase() === (tableName ?? '').toLowerCase() &&
|
|
111
|
+
(!schemaName || (e.SchemaName ?? '').toLowerCase() === schemaName.toLowerCase())
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (!entity) return null;
|
|
115
|
+
|
|
116
|
+
// Load columns from Entity Fields via RunView.
|
|
117
|
+
const columns = await this.loadEntityColumns(entity.ID, params);
|
|
118
|
+
return {
|
|
119
|
+
EntityID: entity.ID,
|
|
120
|
+
EntityName: entity.Name,
|
|
121
|
+
SchemaName: entity.SchemaName ?? '',
|
|
122
|
+
TableName: entity.BaseTable ?? '',
|
|
123
|
+
Description: entity.Description ?? null,
|
|
124
|
+
Columns: columns,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Load column definitions for a given entity ID. */
|
|
129
|
+
private async loadEntityColumns(
|
|
130
|
+
entityID: string,
|
|
131
|
+
params: RunActionParams
|
|
132
|
+
): Promise<ColumnInfo[]> {
|
|
133
|
+
const rv = new RunView();
|
|
134
|
+
const result = await rv.RunView<{
|
|
135
|
+
Name: string;
|
|
136
|
+
Type: string;
|
|
137
|
+
AllowsNull: boolean;
|
|
138
|
+
MaxLength: number | null;
|
|
139
|
+
Description: string | null;
|
|
140
|
+
IsRequired: boolean;
|
|
141
|
+
}>({
|
|
142
|
+
EntityName: 'MJ: Entity Fields',
|
|
143
|
+
ExtraFilter: `EntityID = '${escapeSqlLiteral(entityID)}'`,
|
|
144
|
+
Fields: ['Name', 'Type', 'AllowsNull', 'MaxLength', 'Description', 'IsRequired'],
|
|
145
|
+
OrderBy: 'Sequence ASC',
|
|
146
|
+
ResultType: 'simple',
|
|
147
|
+
}, params.ContextUser);
|
|
148
|
+
|
|
149
|
+
if (!result.Success) return [];
|
|
150
|
+
|
|
151
|
+
return result.Results.map(f => ({
|
|
152
|
+
Name: f.Name,
|
|
153
|
+
Type: f.Type,
|
|
154
|
+
IsNullable: f.AllowsNull,
|
|
155
|
+
MaxLength: f.MaxLength,
|
|
156
|
+
Description: f.Description,
|
|
157
|
+
IsRequired: f.IsRequired,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
}
|