@objectql/core 1.3.1 → 1.5.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +116 -0
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.js +98 -1
- package/dist/loader.js.map +1 -1
- package/dist/repository.d.ts +1 -0
- package/dist/repository.js +28 -3
- package/dist/repository.js.map +1 -1
- package/dist/validator.d.ts +69 -0
- package/dist/validator.js +461 -0
- package/dist/validator.js.map +1 -0
- package/jest.config.js +6 -1
- package/package.json +3 -3
- package/src/app.ts +96 -0
- package/src/index.ts +1 -0
- package/src/loader.ts +108 -1
- package/src/repository.ts +29 -3
- package/src/validator.ts +553 -0
- package/test/action.test.ts +240 -22
- package/test/fixtures/project-with-validation.object.yml +124 -0
- package/test/hook.test.ts +310 -27
- package/test/mock-driver.ts +6 -3
- package/test/validation.test.ts +486 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/loader.ts
CHANGED
|
@@ -101,6 +101,51 @@ export class ObjectLoader {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
// Generic YAML Metadata Loaders
|
|
106
|
+
const metaTypes = ['view', 'form', 'menu', 'permission', 'report', 'workflow', 'validation', 'data'];
|
|
107
|
+
|
|
108
|
+
for (const type of metaTypes) {
|
|
109
|
+
this.use({
|
|
110
|
+
name: type,
|
|
111
|
+
glob: [`**/*.${type}.yml`, `**/*.${type}.yaml`],
|
|
112
|
+
handler: (ctx) => {
|
|
113
|
+
try {
|
|
114
|
+
const doc = yaml.load(ctx.content) as any;
|
|
115
|
+
if (!doc) return;
|
|
116
|
+
|
|
117
|
+
// Use 'name' from doc, or filename base (without extension)
|
|
118
|
+
let id = doc.name;
|
|
119
|
+
if (!id && type !== 'data') {
|
|
120
|
+
const basename = path.basename(ctx.file);
|
|
121
|
+
// e.g. "my-view.view.yml" -> "my-view"
|
|
122
|
+
// Regex: remove .type.yml or .type.yaml
|
|
123
|
+
const re = new RegExp(`\\.${type}\\.(yml|yaml)$`);
|
|
124
|
+
id = basename.replace(re, '');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Data entries might not need a name, but for registry we need an ID.
|
|
128
|
+
// For data, we can use filename if not present.
|
|
129
|
+
if (!id && type === 'data') {
|
|
130
|
+
id = path.basename(ctx.file);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Ensure name is in the doc for consistency
|
|
134
|
+
if (!doc.name) doc.name = id;
|
|
135
|
+
|
|
136
|
+
ctx.registry.register(type, {
|
|
137
|
+
type: type,
|
|
138
|
+
id: id,
|
|
139
|
+
path: ctx.file,
|
|
140
|
+
package: ctx.packageName,
|
|
141
|
+
content: doc
|
|
142
|
+
});
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error(`Error loading ${type} from ${ctx.file}:`, e);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
104
149
|
}
|
|
105
150
|
|
|
106
151
|
use(plugin: LoaderPlugin) {
|
|
@@ -127,9 +172,39 @@ export class ObjectLoader {
|
|
|
127
172
|
}
|
|
128
173
|
|
|
129
174
|
private runPlugin(plugin: LoaderPlugin, dir: string, packageName?: string) {
|
|
175
|
+
// Enforce path conventions:
|
|
176
|
+
// 1. Never scan node_modules (unless explicitly loaded via loadPackage which sets cwd inside it)
|
|
177
|
+
// 2. Ignore build artifacts (dist, build, out) to avoid double-loading metadata if both src and dist exist.
|
|
178
|
+
// Note: If you want to load from 'dist', you must explicitly point the loader to it (e.g. loader.load('./dist')).
|
|
179
|
+
// In that case, the patterns won't match relative to the CWD.
|
|
180
|
+
// Path conventions:
|
|
181
|
+
// 1. Always ignore node_modules and .git
|
|
182
|
+
const ignore = [
|
|
183
|
+
'**/node_modules/**',
|
|
184
|
+
'**/.git/**'
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// 2. Intelligent handling of build artifacts (dist/build)
|
|
188
|
+
// If 'src' exists in the scan directory, we assume it's a Development Environment.
|
|
189
|
+
// In Dev, we ignore 'dist' to avoid duplicate loading (ts in src vs js in dist).
|
|
190
|
+
// In Production (no src), we must NOT ignore 'dist', otherwise we can't load compiled hooks/actions.
|
|
191
|
+
const srcPath = path.join(dir, 'src');
|
|
192
|
+
const hasSrc = fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory();
|
|
193
|
+
|
|
194
|
+
if (hasSrc) {
|
|
195
|
+
ignore.push('**/dist/**', '**/build/**', '**/out/**');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3. User instruction: "src 不行的" (src is not viable for metadata in production)
|
|
199
|
+
// Metadata (.yml) should ideally be placed in 'objects/' or root, not 'src/',
|
|
200
|
+
// to simplify packaging (so you don't need to copy assets from src to dist).
|
|
201
|
+
// However, we do not strictly block 'src' scanning here to avoid breaking dev workflow.
|
|
202
|
+
// The exclusion of 'dist' in dev mode (above) handles the code duality.
|
|
203
|
+
|
|
130
204
|
const files = glob.sync(plugin.glob, {
|
|
131
205
|
cwd: dir,
|
|
132
|
-
absolute: true
|
|
206
|
+
absolute: true,
|
|
207
|
+
ignore
|
|
133
208
|
});
|
|
134
209
|
|
|
135
210
|
for (const file of files) {
|
|
@@ -166,6 +241,38 @@ function registerObject(registry: MetadataRegistry, obj: any, file: string, pack
|
|
|
166
241
|
}
|
|
167
242
|
}
|
|
168
243
|
}
|
|
244
|
+
|
|
245
|
+
// Check for existing object to Merge
|
|
246
|
+
const existing = registry.getEntry('object', obj.name);
|
|
247
|
+
if (existing) {
|
|
248
|
+
const base = existing.content;
|
|
249
|
+
|
|
250
|
+
// Merge Fields: New fields overwrite old ones
|
|
251
|
+
if (obj.fields) {
|
|
252
|
+
base.fields = { ...base.fields, ...obj.fields };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Merge Actions
|
|
256
|
+
if (obj.actions) {
|
|
257
|
+
base.actions = { ...base.actions, ...obj.actions };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Merge Indexes
|
|
261
|
+
if (obj.indexes) {
|
|
262
|
+
base.indexes = { ...base.indexes, ...obj.indexes };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Override Top-level Properties if provided
|
|
266
|
+
if (obj.label) base.label = obj.label;
|
|
267
|
+
if (obj.icon) base.icon = obj.icon;
|
|
268
|
+
if (obj.description) base.description = obj.description;
|
|
269
|
+
if (obj.datasource) base.datasource = obj.datasource;
|
|
270
|
+
|
|
271
|
+
// Update the content reference
|
|
272
|
+
existing.content = base;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
169
276
|
registry.register('object', {
|
|
170
277
|
type: 'object',
|
|
171
278
|
id: obj.name,
|
package/src/repository.ts
CHANGED
|
@@ -39,12 +39,26 @@ export class ObjectRepository {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
private getUserFromContext() {
|
|
43
|
+
if (!this.context.userId) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// Construct user object from context, including relevant properties
|
|
47
|
+
return {
|
|
48
|
+
id: this.context.userId,
|
|
49
|
+
spaceId: this.context.spaceId,
|
|
50
|
+
roles: this.context.roles,
|
|
51
|
+
isSystem: this.context.isSystem
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
async find(query: UnifiedQuery = {}): Promise<any[]> {
|
|
43
56
|
const hookCtx: RetrievalHookContext = {
|
|
44
57
|
...this.context,
|
|
45
58
|
objectName: this.objectName,
|
|
46
59
|
operation: 'find',
|
|
47
60
|
api: this.getHookAPI(),
|
|
61
|
+
user: this.getUserFromContext(),
|
|
48
62
|
state: {},
|
|
49
63
|
query
|
|
50
64
|
};
|
|
@@ -65,7 +79,10 @@ export class ObjectRepository {
|
|
|
65
79
|
...this.context,
|
|
66
80
|
objectName: this.objectName,
|
|
67
81
|
operation: 'find',
|
|
68
|
-
api: this.getHookAPI(),
|
|
82
|
+
api: this.getHookAPI(),
|
|
83
|
+
user: this.getUserFromContext(),
|
|
84
|
+
state: {},
|
|
85
|
+
query: { _id: idOrQuery }
|
|
69
86
|
};
|
|
70
87
|
await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
|
|
71
88
|
|
|
@@ -86,6 +103,7 @@ export class ObjectRepository {
|
|
|
86
103
|
objectName: this.objectName,
|
|
87
104
|
operation: 'count',
|
|
88
105
|
api: this.getHookAPI(),
|
|
106
|
+
user: this.getUserFromContext(),
|
|
89
107
|
state: {},
|
|
90
108
|
query: filters
|
|
91
109
|
};
|
|
@@ -105,6 +123,7 @@ export class ObjectRepository {
|
|
|
105
123
|
operation: 'create',
|
|
106
124
|
state: {},
|
|
107
125
|
api: this.getHookAPI(),
|
|
126
|
+
user: this.getUserFromContext(),
|
|
108
127
|
data: doc
|
|
109
128
|
};
|
|
110
129
|
await this.app.triggerHook('beforeCreate', this.objectName, hookCtx);
|
|
@@ -122,14 +141,17 @@ export class ObjectRepository {
|
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
async update(id: string | number, doc: any, options?: any): Promise<any> {
|
|
144
|
+
const previousData = await this.findOne(id);
|
|
125
145
|
const hookCtx: UpdateHookContext = {
|
|
126
146
|
...this.context,
|
|
127
147
|
objectName: this.objectName,
|
|
128
148
|
operation: 'update',
|
|
129
149
|
state: {},
|
|
130
150
|
api: this.getHookAPI(),
|
|
151
|
+
user: this.getUserFromContext(),
|
|
131
152
|
id,
|
|
132
153
|
data: doc,
|
|
154
|
+
previousData,
|
|
133
155
|
isModified: (field) => hookCtx.data ? Object.prototype.hasOwnProperty.call(hookCtx.data, field) : false
|
|
134
156
|
};
|
|
135
157
|
await this.app.triggerHook('beforeUpdate', this.objectName, hookCtx);
|
|
@@ -142,13 +164,16 @@ export class ObjectRepository {
|
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
async delete(id: string | number): Promise<any> {
|
|
167
|
+
const previousData = await this.findOne(id);
|
|
145
168
|
const hookCtx: MutationHookContext = {
|
|
146
169
|
...this.context,
|
|
147
170
|
objectName: this.objectName,
|
|
148
171
|
operation: 'delete',
|
|
149
172
|
state: {},
|
|
150
173
|
api: this.getHookAPI(),
|
|
151
|
-
|
|
174
|
+
user: this.getUserFromContext(),
|
|
175
|
+
id,
|
|
176
|
+
previousData
|
|
152
177
|
};
|
|
153
178
|
await this.app.triggerHook('beforeDelete', this.objectName, hookCtx);
|
|
154
179
|
|
|
@@ -221,7 +246,8 @@ export class ObjectRepository {
|
|
|
221
246
|
actionName,
|
|
222
247
|
id,
|
|
223
248
|
input: params,
|
|
224
|
-
api
|
|
249
|
+
api,
|
|
250
|
+
user: this.getUserFromContext()
|
|
225
251
|
};
|
|
226
252
|
return await this.app.executeAction(this.objectName, actionName, ctx);
|
|
227
253
|
}
|