@open-mercato/cli 0.7.1-develop.7123.1.25365e718d → 0.7.1-develop.7130.1.fef2396fd8
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 +1 -1
- package/dist/agentic/guides/module-facts.json +128 -128
- package/dist/agentic/guides/module-facts.v2.json +128 -128
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customers/index.md +1 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/lib/generators/entity-class-declarations.js +186 -0
- package/dist/lib/generators/entity-class-declarations.js.map +7 -0
- package/dist/lib/generators/module-entities.js +28 -0
- package/dist/lib/generators/module-entities.js.map +2 -2
- package/package.json +6 -6
- package/src/lib/generators/__tests__/entity-class-declarations.test.ts +287 -0
- package/src/lib/generators/__tests__/module-entities.test.ts +219 -0
- package/src/lib/generators/entity-class-declarations.ts +255 -0
- package/src/lib/generators/module-entities.ts +54 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { parseEntityClassNames } from '../entity-class-declarations'
|
|
5
|
+
|
|
6
|
+
let tmpDir: string
|
|
7
|
+
|
|
8
|
+
function writeSource(content: string, fileName = 'entities.ts'): string {
|
|
9
|
+
const filePath = path.join(tmpDir, fileName)
|
|
10
|
+
fs.writeFileSync(filePath, content)
|
|
11
|
+
return filePath
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entity-class-declarations-test-'))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('parseEntityClassNames', () => {
|
|
23
|
+
it('collects exported classes decorated with @Entity()', () => {
|
|
24
|
+
const filePath = writeSource(`
|
|
25
|
+
import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
|
|
26
|
+
|
|
27
|
+
@Entity({ tableName: 'invoices' })
|
|
28
|
+
export class Invoice {
|
|
29
|
+
@PrimaryKey({ type: 'uuid' })
|
|
30
|
+
id!: string
|
|
31
|
+
}
|
|
32
|
+
`)
|
|
33
|
+
|
|
34
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('looks past comments and stacked decorators', () => {
|
|
38
|
+
const filePath = writeSource(`
|
|
39
|
+
import { Entity, Index, Unique } from '@mikro-orm/decorators/legacy'
|
|
40
|
+
|
|
41
|
+
@Entity({ tableName: 'invoices' })
|
|
42
|
+
// The unique constraint bounds the lookup.
|
|
43
|
+
@Unique({ properties: ['number'] })
|
|
44
|
+
@Index({
|
|
45
|
+
name: 'invoices_number_idx',
|
|
46
|
+
expression: 'create index "invoices_number_idx" on "invoices" ("number")',
|
|
47
|
+
})
|
|
48
|
+
export class Invoice {}
|
|
49
|
+
`)
|
|
50
|
+
|
|
51
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('ignores exported classes that are not entities', () => {
|
|
55
|
+
const filePath = writeSource(`
|
|
56
|
+
import { Entity } from '@mikro-orm/decorators/legacy'
|
|
57
|
+
|
|
58
|
+
export class InvoiceBuilder {}
|
|
59
|
+
|
|
60
|
+
@Entity({ tableName: 'invoices' })
|
|
61
|
+
export class Invoice {}
|
|
62
|
+
`)
|
|
63
|
+
|
|
64
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('ignores entity classes that are not exported', () => {
|
|
68
|
+
const filePath = writeSource(`
|
|
69
|
+
import { Entity } from '@mikro-orm/decorators/legacy'
|
|
70
|
+
|
|
71
|
+
@Entity({ tableName: 'invoices' })
|
|
72
|
+
class Invoice {}
|
|
73
|
+
`)
|
|
74
|
+
|
|
75
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('handles a local export list', () => {
|
|
79
|
+
const filePath = writeSource(`
|
|
80
|
+
import { Entity } from '@mikro-orm/decorators/legacy'
|
|
81
|
+
|
|
82
|
+
@Entity({ tableName: 'invoices' })
|
|
83
|
+
class Invoice {}
|
|
84
|
+
|
|
85
|
+
export { Invoice }
|
|
86
|
+
`)
|
|
87
|
+
|
|
88
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('accepts a bare @Entity decorator', () => {
|
|
92
|
+
const filePath = writeSource(`
|
|
93
|
+
import { Entity } from '@mikro-orm/decorators/legacy'
|
|
94
|
+
|
|
95
|
+
@Entity
|
|
96
|
+
export class Invoice {}
|
|
97
|
+
`)
|
|
98
|
+
|
|
99
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns nothing for an unreadable file instead of throwing', () => {
|
|
103
|
+
expect(parseEntityClassNames(path.join(tmpDir, 'missing.ts'))).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('recognises an aliased Entity import', () => {
|
|
107
|
+
const filePath = writeSource(`
|
|
108
|
+
import { Entity as OrmEntity } from '@mikro-orm/decorators/legacy'
|
|
109
|
+
|
|
110
|
+
@OrmEntity({ tableName: 'invoices' })
|
|
111
|
+
export class Invoice {}
|
|
112
|
+
`)
|
|
113
|
+
|
|
114
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('recognises a namespace-qualified Entity decorator', () => {
|
|
118
|
+
const filePath = writeSource(`
|
|
119
|
+
import * as orm from '@mikro-orm/decorators/legacy'
|
|
120
|
+
|
|
121
|
+
@orm.Entity({ tableName: 'invoices' })
|
|
122
|
+
export class Invoice {}
|
|
123
|
+
`)
|
|
124
|
+
|
|
125
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('does not treat an unrelated decorator as an entity', () => {
|
|
129
|
+
const filePath = writeSource(`
|
|
130
|
+
@Injectable()
|
|
131
|
+
export class InvoiceService {}
|
|
132
|
+
`)
|
|
133
|
+
|
|
134
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('ignores an Entity decorator imported from an unrelated library', () => {
|
|
138
|
+
// Any library may export an `Entity` decorator; only MikroORM's collides in the ORM
|
|
139
|
+
// metadata registry, so only a decorator bound to a @mikro-orm/* import counts.
|
|
140
|
+
const filePath = writeSource(`
|
|
141
|
+
import { Entity } from 'some-other-orm'
|
|
142
|
+
|
|
143
|
+
@Entity({ tableName: 'invoices' })
|
|
144
|
+
export class Invoice {}
|
|
145
|
+
`)
|
|
146
|
+
|
|
147
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('ignores a namespace-qualified Entity decorator from an unrelated library', () => {
|
|
151
|
+
const filePath = writeSource(`
|
|
152
|
+
import * as other from 'some-other-orm'
|
|
153
|
+
|
|
154
|
+
@other.Entity({ tableName: 'invoices' })
|
|
155
|
+
export class Invoice {}
|
|
156
|
+
`)
|
|
157
|
+
|
|
158
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('ignores an Entity decorator with no import to bind it', () => {
|
|
162
|
+
const filePath = writeSource(`
|
|
163
|
+
@Entity({ tableName: 'invoices' })
|
|
164
|
+
export class Invoice {}
|
|
165
|
+
`)
|
|
166
|
+
|
|
167
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('parses a .tsx entity file as JSX rather than dropping declarations', () => {
|
|
171
|
+
// MODULE_CODE_EXTENSIONS accepts .tsx, where `<T>` is JSX, not a type assertion.
|
|
172
|
+
const filePath = writeSource(`
|
|
173
|
+
import { Entity } from '@mikro-orm/decorators/legacy'
|
|
174
|
+
|
|
175
|
+
export const icon = () => <span>invoice</span>
|
|
176
|
+
|
|
177
|
+
@Entity({ tableName: 'invoices' })
|
|
178
|
+
export class Invoice {}
|
|
179
|
+
`, 'entities.tsx')
|
|
180
|
+
|
|
181
|
+
expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('parseEntityClassNames on compiled package output', () => {
|
|
186
|
+
// A package that ships dist only hands the generator its built entity file, where a
|
|
187
|
+
// decorated class has become a class expression plus a decorator-helper call. Without
|
|
188
|
+
// this the standalone generator sees no entities at all and misses every collision.
|
|
189
|
+
it('reads an esbuild ESM emit', () => {
|
|
190
|
+
const filePath = writeSource(`
|
|
191
|
+
var __decorateClass = (decorators, target) => target;
|
|
192
|
+
import { Entity, PrimaryKey } from "@mikro-orm/decorators/legacy";
|
|
193
|
+
let ApiKey = class {
|
|
194
|
+
};
|
|
195
|
+
__decorateClass([
|
|
196
|
+
PrimaryKey({ type: "uuid" })
|
|
197
|
+
], ApiKey.prototype, "id", 2);
|
|
198
|
+
ApiKey = __decorateClass([
|
|
199
|
+
Entity({ tableName: "api_keys" })
|
|
200
|
+
], ApiKey);
|
|
201
|
+
export {
|
|
202
|
+
ApiKey
|
|
203
|
+
};
|
|
204
|
+
`, 'entities.js')
|
|
205
|
+
|
|
206
|
+
expect(parseEntityClassNames(filePath)).toEqual(['ApiKey'])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('reads a tsc CommonJS emit', () => {
|
|
210
|
+
const filePath = writeSource(`
|
|
211
|
+
"use strict";
|
|
212
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
213
|
+
exports.ApiKey = void 0;
|
|
214
|
+
const legacy_1 = require("@mikro-orm/decorators/legacy");
|
|
215
|
+
let ApiKey = class ApiKey {
|
|
216
|
+
};
|
|
217
|
+
ApiKey = __decorate([
|
|
218
|
+
(0, legacy_1.Entity)({ tableName: "api_keys" })
|
|
219
|
+
], ApiKey);
|
|
220
|
+
exports.ApiKey = ApiKey;
|
|
221
|
+
`, 'entities.js')
|
|
222
|
+
|
|
223
|
+
expect(parseEntityClassNames(filePath)).toEqual(['ApiKey'])
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('reads a bundler export table', () => {
|
|
227
|
+
const filePath = writeSource(`
|
|
228
|
+
import { Entity } from "@mikro-orm/core";
|
|
229
|
+
var entities_exports = {};
|
|
230
|
+
__export(entities_exports, {
|
|
231
|
+
ApiKey: () => ApiKey
|
|
232
|
+
});
|
|
233
|
+
let ApiKey = class {
|
|
234
|
+
};
|
|
235
|
+
ApiKey = __decorateClass([
|
|
236
|
+
Entity({ tableName: "api_keys" })
|
|
237
|
+
], ApiKey);
|
|
238
|
+
`, 'entities.js')
|
|
239
|
+
|
|
240
|
+
expect(parseEntityClassNames(filePath)).toEqual(['ApiKey'])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('ignores a compiled class that is never exported', () => {
|
|
244
|
+
const filePath = writeSource(`
|
|
245
|
+
import { Entity } from "@mikro-orm/decorators/legacy";
|
|
246
|
+
let Internal = class {
|
|
247
|
+
};
|
|
248
|
+
Internal = __decorateClass([
|
|
249
|
+
Entity({ tableName: "internal" })
|
|
250
|
+
], Internal);
|
|
251
|
+
`, 'entities.js')
|
|
252
|
+
|
|
253
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('ignores a compiled class decorated by an unrelated library', () => {
|
|
257
|
+
const filePath = writeSource(`
|
|
258
|
+
import { Entity } from "some-other-orm";
|
|
259
|
+
let Invoice = class {
|
|
260
|
+
};
|
|
261
|
+
Invoice = __decorateClass([
|
|
262
|
+
Entity({ tableName: "invoices" })
|
|
263
|
+
], Invoice);
|
|
264
|
+
export {
|
|
265
|
+
Invoice
|
|
266
|
+
};
|
|
267
|
+
`, 'entities.js')
|
|
268
|
+
|
|
269
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('ignores property decorators, which share the helper call', () => {
|
|
273
|
+
const filePath = writeSource(`
|
|
274
|
+
import { Entity, Property } from "@mikro-orm/decorators/legacy";
|
|
275
|
+
let Helper = class {
|
|
276
|
+
};
|
|
277
|
+
__decorateClass([
|
|
278
|
+
Property({ type: "text" })
|
|
279
|
+
], Helper.prototype, "name", 2);
|
|
280
|
+
export {
|
|
281
|
+
Helper
|
|
282
|
+
};
|
|
283
|
+
`, 'entities.js')
|
|
284
|
+
|
|
285
|
+
expect(parseEntityClassNames(filePath)).toEqual([])
|
|
286
|
+
})
|
|
287
|
+
})
|
|
@@ -135,3 +135,222 @@ describe('generateModuleEntities', () => {
|
|
|
135
135
|
expect(secondStat.mtimeMs).toBe(firstStat.mtimeMs)
|
|
136
136
|
})
|
|
137
137
|
})
|
|
138
|
+
|
|
139
|
+
describe('generateModuleEntities duplicate entity class names', () => {
|
|
140
|
+
const entitySource = (className: string, tableName: string) =>
|
|
141
|
+
`import { Entity } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: '${tableName}' })\nexport class ${className} {}\n`
|
|
142
|
+
|
|
143
|
+
let warnSpy: jest.SpyInstance
|
|
144
|
+
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
afterEach(() => {
|
|
150
|
+
warnSpy.mockRestore()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
function moduleWithEntities(id: string, content: string): ModuleEntry {
|
|
154
|
+
touchFile(path.join(tmpDir, 'packages', 'core', 'src', 'modules', id, 'data', 'entities.ts'), content)
|
|
155
|
+
return { id, from: '@open-mercato/core' }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
it('warns when two modules declare the same entity class name, and still generates', async () => {
|
|
159
|
+
const modules = [
|
|
160
|
+
moduleWithEntities('billing', entitySource('Invoice', 'billing_invoices')),
|
|
161
|
+
moduleWithEntities('subscriptions', entitySource('Invoice', 'subscription_invoices')),
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
const result = await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
|
|
165
|
+
|
|
166
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
167
|
+
const message = warnSpy.mock.calls[0][0] as string
|
|
168
|
+
expect(message).toContain('[Entities Warning]')
|
|
169
|
+
expect(message).toContain('Invoice')
|
|
170
|
+
expect(message).toContain('billing')
|
|
171
|
+
expect(message).toContain('subscriptions')
|
|
172
|
+
// Warning-only by design: generation still succeeds.
|
|
173
|
+
expect(result.errors).toEqual([])
|
|
174
|
+
expect(readGenerated(tmpDir)).toContain('enhanceEntities')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('stays silent when entity class names are unique', async () => {
|
|
178
|
+
const modules = [
|
|
179
|
+
moduleWithEntities('billing', entitySource('Invoice', 'billing_invoices')),
|
|
180
|
+
moduleWithEntities('subscriptions', entitySource('Subscription', 'subscriptions')),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
|
|
184
|
+
|
|
185
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('reads the app override, not the package file, when an app supplies a package module\'s entities', async () => {
|
|
189
|
+
// The registry imports the app override for such a module, so the check must read the
|
|
190
|
+
// same file — parsing the package tree would report names that are never registered.
|
|
191
|
+
touchFile(
|
|
192
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'billing', 'data', 'entities.ts'),
|
|
193
|
+
entitySource('PackageInvoice', 'billing_invoices'),
|
|
194
|
+
)
|
|
195
|
+
touchFile(
|
|
196
|
+
path.join(tmpDir, 'app', 'src', 'modules', 'billing', 'data', 'entities.ts'),
|
|
197
|
+
entitySource('Invoice', 'billing_invoices'),
|
|
198
|
+
)
|
|
199
|
+
const modules: ModuleEntry[] = [
|
|
200
|
+
{ id: 'billing', from: '@open-mercato/core' },
|
|
201
|
+
moduleWithEntities('subscriptions', entitySource('Invoice', 'subscription_invoices')),
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
const result = await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
|
|
205
|
+
|
|
206
|
+
expect(result.errors).toEqual([])
|
|
207
|
+
expect(readGenerated(tmpDir)).toContain('from "@/modules/billing/data/entities"')
|
|
208
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
209
|
+
const message = warnSpy.mock.calls[0][0] as string
|
|
210
|
+
expect(message).toContain('Invoice')
|
|
211
|
+
expect(message).toContain(path.join('app', 'src', 'modules', 'billing', 'data', 'entities.ts'))
|
|
212
|
+
expect(message).not.toContain('PackageInvoice')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('stays silent when only one of the colliding classes is an entity', async () => {
|
|
216
|
+
const modules = [
|
|
217
|
+
moduleWithEntities('billing', entitySource('Invoice', 'billing_invoices')),
|
|
218
|
+
moduleWithEntities('subscriptions', 'export class Invoice {}\n'),
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
|
|
222
|
+
|
|
223
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('generateModuleEntities duplicate detection in standalone installs', () => {
|
|
228
|
+
// A standalone install resolves a package module to its compiled dist tree, where a
|
|
229
|
+
// decorated class is no longer a class declaration. Published packages ship
|
|
230
|
+
// src/modules too, so parsing prefers that mirror and falls back to the compiled file.
|
|
231
|
+
const PACKAGE = '@open-mercato/core'
|
|
232
|
+
|
|
233
|
+
let warnSpy: jest.SpyInstance
|
|
234
|
+
|
|
235
|
+
beforeEach(() => {
|
|
236
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
afterEach(() => {
|
|
240
|
+
warnSpy.mockRestore()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
function packageRoot(): string {
|
|
244
|
+
return path.join(tmpDir, 'app', 'node_modules', PACKAGE)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function createStandaloneResolver(enabled: ModuleEntry[]): PackageResolver {
|
|
248
|
+
const outputDir = path.join(tmpDir, 'app', '.mercato', 'generated')
|
|
249
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
isMonorepo: () => false,
|
|
253
|
+
getRootDir: () => path.join(tmpDir, 'app'),
|
|
254
|
+
getAppDir: () => path.join(tmpDir, 'app'),
|
|
255
|
+
getOutputDir: () => outputDir,
|
|
256
|
+
getModulesConfigPath: () => path.join(tmpDir, 'app', 'src', 'modules.ts'),
|
|
257
|
+
discoverPackages: () => [],
|
|
258
|
+
loadEnabledModules: () => enabled,
|
|
259
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
260
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
261
|
+
pkgBase: path.join(packageRoot(), 'dist', 'modules', entry.id),
|
|
262
|
+
}),
|
|
263
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
264
|
+
appBase: `@/modules/${entry.id}`,
|
|
265
|
+
pkgBase: `${PACKAGE}/modules/${entry.id}`,
|
|
266
|
+
}),
|
|
267
|
+
getPackageOutputDir: () => outputDir,
|
|
268
|
+
getPackageRoot: () => packageRoot(),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const compiledEntity = (className: string, tableName: string) => `
|
|
273
|
+
import { Entity } from "@mikro-orm/decorators/legacy";
|
|
274
|
+
let ${className} = class {
|
|
275
|
+
};
|
|
276
|
+
${className} = __decorateClass([
|
|
277
|
+
Entity({ tableName: "${tableName}" })
|
|
278
|
+
], ${className});
|
|
279
|
+
export {
|
|
280
|
+
${className}
|
|
281
|
+
};
|
|
282
|
+
`
|
|
283
|
+
|
|
284
|
+
const sourceEntity = (className: string, tableName: string) =>
|
|
285
|
+
`import { Entity } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: '${tableName}' })\nexport class ${className} {}\n`
|
|
286
|
+
|
|
287
|
+
function distModule(id: string, className: string, tableName: string): ModuleEntry {
|
|
288
|
+
touchFile(path.join(packageRoot(), 'dist', 'modules', id, 'data', 'entities.js'), compiledEntity(className, tableName))
|
|
289
|
+
return { id, from: PACKAGE }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function withSourceMirror(id: string, className: string, tableName: string): void {
|
|
293
|
+
touchFile(path.join(packageRoot(), 'src', 'modules', id, 'data', 'entities.ts'), sourceEntity(className, tableName))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
it('detects a package-to-package collision through the shipped source mirror', async () => {
|
|
297
|
+
const modules = [
|
|
298
|
+
distModule('billing', 'Invoice', 'billing_invoices'),
|
|
299
|
+
distModule('subscriptions', 'Invoice', 'subscription_invoices'),
|
|
300
|
+
]
|
|
301
|
+
withSourceMirror('billing', 'Invoice', 'billing_invoices')
|
|
302
|
+
withSourceMirror('subscriptions', 'Invoice', 'subscription_invoices')
|
|
303
|
+
|
|
304
|
+
const result = await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
|
|
305
|
+
|
|
306
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
307
|
+
const message = warnSpy.mock.calls[0][0] as string
|
|
308
|
+
expect(message).toContain('Invoice')
|
|
309
|
+
expect(message).toContain(path.join('src', 'modules', 'billing', 'data', 'entities.ts'))
|
|
310
|
+
expect(result.errors).toEqual([])
|
|
311
|
+
// The runtime import still points at the compiled package entry.
|
|
312
|
+
expect(readGenerated(tmpDir)).toContain(`from "${PACKAGE}/modules/billing/data/entities"`)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('detects the collision from compiled output when a package ships dist only', async () => {
|
|
316
|
+
const modules = [
|
|
317
|
+
distModule('billing', 'Invoice', 'billing_invoices'),
|
|
318
|
+
distModule('subscriptions', 'Invoice', 'subscription_invoices'),
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
|
|
322
|
+
|
|
323
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
324
|
+
const message = warnSpy.mock.calls[0][0] as string
|
|
325
|
+
expect(message).toContain('Invoice')
|
|
326
|
+
expect(message).toContain(path.join('dist', 'modules', 'subscriptions', 'data', 'entities.js'))
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('detects a package-to-app collision', async () => {
|
|
330
|
+
const modules = [
|
|
331
|
+
distModule('billing', 'Invoice', 'billing_invoices'),
|
|
332
|
+
{ id: 'reporting', from: '@app' } as ModuleEntry,
|
|
333
|
+
]
|
|
334
|
+
withSourceMirror('billing', 'Invoice', 'billing_invoices')
|
|
335
|
+
touchFile(
|
|
336
|
+
path.join(tmpDir, 'app', 'src', 'modules', 'reporting', 'data', 'entities.ts'),
|
|
337
|
+
sourceEntity('Invoice', 'reporting_invoices'),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
|
|
341
|
+
|
|
342
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
343
|
+
expect(warnSpy.mock.calls[0][0] as string).toContain('reporting')
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('stays silent when compiled package entity class names are unique', async () => {
|
|
347
|
+
const modules = [
|
|
348
|
+
distModule('billing', 'Invoice', 'billing_invoices'),
|
|
349
|
+
distModule('subscriptions', 'Subscription', 'subscriptions'),
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
|
|
353
|
+
|
|
354
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
355
|
+
})
|
|
356
|
+
})
|