@workos/oagen-emitters 0.16.0 → 0.16.1

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.
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
2
+ import type { EmitterContext, ApiSpec, Model, Service } from '@workos/oagen';
3
3
  import { defaultSdkBehavior } from '@workos/oagen';
4
- import { modelHasNewFields } from '../../src/node/utils.js';
4
+ import { modelHasNewFields, createServiceDirResolver } from '../../src/node/utils.js';
5
+ import { emptyLiveSurface, setActiveLiveSurface } from '../../src/node/live-surface.js';
5
6
 
6
7
  const emptySpec: ApiSpec = {
7
8
  name: 'Test',
@@ -87,3 +88,157 @@ describe('modelHasNewFields', () => {
87
88
  expect(modelHasNewFields(model, ctxWithSurface)).toBe(true);
88
89
  });
89
90
  });
91
+
92
+ describe('createServiceDirResolver owned-service dependency reassignment', () => {
93
+ const retentionModel: Model = {
94
+ name: 'AuditLogsRetention',
95
+ fields: [{ name: 'retention_period_in_days', type: { kind: 'primitive', type: 'integer' }, required: true }],
96
+ };
97
+
98
+ function retentionOp(name: string, path: string) {
99
+ return {
100
+ name,
101
+ httpMethod: 'get' as const,
102
+ path,
103
+ pathParams: [],
104
+ queryParams: [],
105
+ headerParams: [],
106
+ response: { kind: 'model' as const, name: 'AuditLogsRetention' },
107
+ errors: [],
108
+ injectIdempotencyKey: false,
109
+ };
110
+ }
111
+
112
+ // Organizations comes first, so first-reference-wins assignment parks the
113
+ // model in `organizations/` even though only AuditLogs is owned this run.
114
+ const services: Service[] = [
115
+ { name: 'Organizations', operations: [retentionOp('getRetention', '/organizations/{id}/retention')] },
116
+ { name: 'AuditLogs', operations: [retentionOp('getAuditLogsRetention', '/audit_logs/retention')] },
117
+ ];
118
+
119
+ function makeCtx(): EmitterContext {
120
+ return {
121
+ ...ctx,
122
+ spec: { ...emptySpec, models: [retentionModel], services },
123
+ emitterOptions: { ownedServices: ['AuditLogs'] },
124
+ } as EmitterContext;
125
+ }
126
+
127
+ it('reassigns dependency models of owned services out of unemittable directories', () => {
128
+ const surface = emptyLiveSurface();
129
+ surface.files.add('src/workos.ts'); // existing SDK
130
+ setActiveLiveSurface(surface);
131
+ try {
132
+ const testCtx = makeCtx();
133
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
134
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('audit-logs');
135
+ } finally {
136
+ setActiveLiveSurface(emptyLiveSurface());
137
+ }
138
+ });
139
+
140
+ it('leaves the assignment alone when the interface already exists on disk', () => {
141
+ const surface = emptyLiveSurface();
142
+ surface.files.add('src/workos.ts');
143
+ surface.files.add('src/organizations/interfaces/audit-logs-retention.interface.ts');
144
+ setActiveLiveSurface(surface);
145
+ try {
146
+ const testCtx = makeCtx();
147
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
148
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('organizations');
149
+ } finally {
150
+ setActiveLiveSurface(emptyLiveSurface());
151
+ }
152
+ });
153
+
154
+ it('does not reassign when no services are owned', () => {
155
+ const surface = emptyLiveSurface();
156
+ surface.files.add('src/workos.ts');
157
+ setActiveLiveSurface(surface);
158
+ try {
159
+ const testCtx = { ...makeCtx(), emitterOptions: {} } as EmitterContext;
160
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
161
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('organizations');
162
+ } finally {
163
+ setActiveLiveSurface(emptyLiveSurface());
164
+ }
165
+ });
166
+
167
+ it('does not reassign in greenfield mode where every directory is emittable', () => {
168
+ const testCtx = makeCtx();
169
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
170
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('organizations');
171
+ });
172
+
173
+ it('reassigns the full transitive closure when ops are re-mounted onto an owned service', () => {
174
+ // Real instance: GET/PUT /organizations/{organizationId}/audit_logs_retention
175
+ // live on the IR Organizations service but are MOUNTED on AuditLogs via
176
+ // resolvedOperations. Walking only IR services misses them entirely, so
177
+ // `AuditLogsRetention` (and everything it references) stays assigned to
178
+ // the unemittable organizations dir and is never emitted anywhere.
179
+ const nestedModel: Model = {
180
+ name: 'RetentionPolicy',
181
+ fields: [{ name: 'kind', type: { kind: 'primitive', type: 'string' }, required: true }],
182
+ };
183
+ const retentionWithNested: Model = {
184
+ ...retentionModel,
185
+ fields: [
186
+ ...retentionModel.fields,
187
+ { name: 'policy', type: { kind: 'model', name: 'RetentionPolicy' }, required: true },
188
+ ],
189
+ };
190
+ const listOrgsOp = {
191
+ name: 'listOrganizations',
192
+ httpMethod: 'get' as const,
193
+ path: '/organizations',
194
+ pathParams: [],
195
+ queryParams: [],
196
+ headerParams: [],
197
+ response: { kind: 'primitive' as const, type: 'unknown' as const },
198
+ errors: [],
199
+ injectIdempotencyKey: false,
200
+ };
201
+ const mountedRetentionOp = retentionOp(
202
+ 'getAuditLogsRetention',
203
+ '/organizations/{organizationId}/audit_logs_retention',
204
+ );
205
+ const orgService: Service = { name: 'Organizations', operations: [listOrgsOp, mountedRetentionOp] };
206
+ const mountedServices: Service[] = [orgService];
207
+
208
+ const surface = emptyLiveSurface();
209
+ surface.files.add('src/workos.ts'); // existing SDK
210
+ setActiveLiveSurface(surface);
211
+ try {
212
+ const testCtx = {
213
+ ...ctx,
214
+ spec: { ...emptySpec, models: [retentionWithNested, nestedModel], services: mountedServices },
215
+ emitterOptions: { ownedServices: ['AuditLogs'] },
216
+ resolvedOperations: [
217
+ {
218
+ operation: listOrgsOp,
219
+ service: orgService,
220
+ methodName: 'list_organizations',
221
+ mountOn: 'Organizations',
222
+ },
223
+ {
224
+ operation: mountedRetentionOp,
225
+ service: orgService,
226
+ methodName: 'get_audit_logs_retention',
227
+ mountOn: 'AuditLogs',
228
+ },
229
+ ],
230
+ } as unknown as EmitterContext;
231
+ const { modelToService, resolveDir } = createServiceDirResolver(
232
+ [retentionWithNested, nestedModel],
233
+ mountedServices,
234
+ testCtx,
235
+ );
236
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('audit-logs');
237
+ // Nested dependency N follows M into the owned dir — the closure must
238
+ // not stop at the directly-referenced model.
239
+ expect(resolveDir(modelToService.get('RetentionPolicy'))).toBe('audit-logs');
240
+ } finally {
241
+ setActiveLiveSurface(emptyLiveSurface());
242
+ }
243
+ });
244
+ });