Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions scripts/__tests__/pipes-generation.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

const repoRoot = fileURLToPath(new URL('../../', import.meta.url));
const oagen = (...args) => execFileSync('npx', ['--no-install', 'oagen', ...args], {
cwd: repoRoot,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
timeout: 120_000,
});

test('organization and user data providers mount onto Pipes using the real policy', () => {
const operations = JSON.parse(oagen('resolve', '--spec', 'spec/open-api-spec.yaml', '--format', 'json'));
for (const service of ['OrganizationsDataProviders', 'UserManagementDataProviders']) {
const mounted = operations.filter((operation) => operation.service === service);
assert.ok(mounted.length > 0, `expected operations for ${service}`);
for (const operation of mounted) {
assert.equal(operation.mountOn, 'Pipes', `${operation.method} ${operation.path}`);
}
}
});

test('Pipes-scoped Node generation puts organization methods and models under src/pipes', (t) => {
const output = mkdtempSync(join(tmpdir(), 'openapi-pipes-generation-'));
t.after(() => rmSync(output, { recursive: true, force: true }));
oagen('generate', '--spec', 'spec/open-api-spec.yaml', '--lang', 'node',
'--namespace', 'workos', '--services', 'Pipes', '--output', output);

const pipes = readFileSync(join(output, 'src/pipes/pipes.ts'), 'utf8');
for (const method of [
'getOrganizationConnectedAccount',
'createOrganizationConnectedAccount',
'updateOrganizationConnectedAccount',
'deleteOrganizationConnectedAccount',
'listOrganizationDataProviders',
'getUserConnectedAccount',
]) {
assert.match(pipes, new RegExp(`async ${method}\\(`));
const file = method.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
assert.ok(existsSync(join(output, `src/pipes/interfaces/${file}-options.interface.ts`)), method);
}
for (const model of ['connected-account', 'connected-account-input', 'data-integrations-list-response']) {
for (const [folder, suffix] of [['interfaces', 'interface'], ['serializers', 'serializer']]) {
const file = `src/pipes/${folder}/${model}.${suffix}.ts`;
assert.ok(existsSync(join(output, file)), `expected ${file}`);
}
}
assert.doesNotMatch(pipes, /\.\.\/(organizations-data-providers|user-management-data-providers)\//);
assert.equal(existsSync(join(output, 'src/organizations-data-providers')), false);
assert.equal(existsSync(join(output, 'src/user-management-data-providers')), false);
});
74 changes: 74 additions & 0 deletions scripts/__tests__/sdk-release-metadata.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
factsFromCompat,
factsFromDiff,
groupFacts,
publicScopeFromService,
renderChangelogMarkdown,
scopesForServices,
} from '../sdk-release-metadata.mjs';
Expand Down Expand Up @@ -84,6 +85,79 @@ test('scopesForServices maps staged post-mount names to changelog scope keys', (
assert.deepEqual(scopesForServices('AgentsRegistrations,AgentsBlueprintsTokens'), new Set(['agents']));
});

test('organization and user data providers share the Pipes release scope', () => {
for (const service of ['OrganizationsDataProviders', 'UserManagementDataProviders']) {
assert.equal(publicScopeFromService(service), 'pipes');
const facts = factsFromDiff({ changes: [{ kind: 'service-added', name: service }] }, EMPTY_INDEXES);
assert.equal(facts[0].scope, 'pipes');
assert.ok(scopesForServices('Pipes').has(facts[0].scope));
}
});

for (const [name, scope] of [
['ConnectedAccount', 'pipes'],
['ConnectedAccountInput', 'pipes'],
['ConnectedAccountAuthMethod', 'pipes'],
['ConnectedAccountConnectionRole', 'pipes'],
['GetOrganizationConnectedAccountOptions', 'pipes'],
['Connection', 'sso'],
['ConnectionType', 'sso'],
['ConnectApplication', 'connect'],
['ApplicationCredentials', 'connect'],
]) {
test(`connected-account classification keeps ${name} in ${scope}`, () => {
const [fact] = factsFromDiff({ changes: [{ kind: 'model-added', name }] }, EMPTY_INDEXES);
assert.equal(fact.scope, scope);
const compat = factsFromCompat(compatBreak(`${name}.from_dict`), [], EMPTY_INDEXES);
assert.equal(compat[0].scope, scope);
});
}

test('ConnectedAccount model and enum release metadata agree with organization Pipes ownership', () => {
const indexes = buildIndexes([{
services: [{
name: 'OrganizationsDataProviders',
operations: [{ name: 'getOrganizationConnectedAccount', response: { kind: 'model', name: 'ConnectedAccount' } }],
}],
models: [{
name: 'ConnectedAccount',
fields: [{ name: 'auth_method', type: { kind: 'enum', name: 'ConnectedAccountAuthMethod' } }],
}],
enums: [{ name: 'ConnectedAccountAuthMethod', values: [] }],
}]);
const facts = factsFromDiff({ changes: [
{ kind: 'model-added', name: 'ConnectedAccount' },
{ kind: 'enum-added', name: 'ConnectedAccountAuthMethod' },
] }, indexes);
for (const fact of facts) {
assert.equal(fact.scope, 'pipes');
assert.equal(fact.scope_source, 'name_and_ir');
assert.deepEqual(fact.scope_candidates, ['pipes']);
}
const entries = entriesFromGroups(groupFacts(facts), []);
assert.equal(entries.length, 1);
assert.equal(entries[0].scope, 'pipes');
assert.equal(entries[0].docs_url, 'https://workos.com/docs/reference/pipes');
});

test('connected-account files are attributed to Pipes rather than Connect', () => {
// Unrelated symbols leave attribution to the file classifier alone.
const facts = factsFromDiff({ changes: [
{ kind: 'model-added', name: 'DataIntegration' },
{ kind: 'model-added', name: 'ConnectApplication' },
] }, EMPTY_INDEXES);
const pipesFiles = [
'workos/types/connected_account.py',
'workos/types/connected_account_auth_method.py',
'Models/ConnectedAccount.cs',
'src/pipes/interfaces/get-organization-connected-account-options.interface.ts',
];
const connectFiles = ['src/connect/interfaces/application.interface.ts'];
const entries = entriesFromGroups(groupFacts(facts), [...pipesFiles, ...connectFiles]);
assert.deepEqual(entries.find((entry) => entry.scope === 'pipes').file_paths, pipesFiles);
assert.deepEqual(entries.find((entry) => entry.scope === 'connect').file_paths, connectFiles);
});

test('scopesForServices returns null for an empty/absent selection (full generation keeps every scope)', () => {
assert.equal(scopesForServices(undefined), null);
assert.equal(scopesForServices(''), null);
Expand Down
5 changes: 4 additions & 1 deletion scripts/sdk-release-metadata.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const SERVICE_SCOPE_OVERRIDES = new Map(
FeatureFlagsTargets: 'feature_flags',
MultiFactorAuthChallenges: 'multi_factor_auth',
OrganizationsApiKeys: 'api_keys',
OrganizationsDataProviders: 'pipes',
OrganizationsFeatureFlags: 'feature_flags',
Permissions: 'authorization',
PipesProvider: 'pipes',
Expand Down Expand Up @@ -417,7 +418,8 @@ export function scopesForServices(servicesArg) {
function scopeFromName(name) {
if (!name) return 'sdk';

if (/DataIntegration|Pipe/.test(name)) return 'pipes';
// Connected accounts belong to Pipes, not the broader Connect* family below.
if (/ConnectedAccount|DataIntegration|Pipe/.test(name)) return 'pipes';
if (/SessionAuthenticate/.test(name)) return 'user_management';
if (/WebhookEndpointEvents|Webhook/.test(name)) return 'webhooks';
if (/^(ApiKey|ExpireApiKey|OrganizationApiKey|UserApiKey)/.test(name)) return 'api_keys';
Expand Down Expand Up @@ -494,6 +496,7 @@ function scopeFromFile(path) {
if (/directorysync|directory_sync|dsync/.test(normalized)) return 'directory_sync';
if (/radar/.test(normalized)) return 'radar';
if (/vault|vaultobject/.test(normalized)) return 'vault';
if (/connectedaccount|dataintegration|pipes/.test(normalized)) return 'pipes';
if (/connect|applicationcredential|externalauth|userobject/.test(normalized)) return 'connect';
if (/featureflag|featureflags|feature_flags/.test(normalized)) return 'feature_flags';
if (/multifactorauth|multi_factor_auth|mfa/.test(normalized)) return 'multi_factor_auth';
Expand Down
1 change: 1 addition & 0 deletions src/policy/mount-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const mountRules: Record<string, string> = {
'UserManagementOrganizationMembership*': 'OrganizationMembership',

// Pipes / Data Providers -> Pipes
OrganizationsDataProviders: 'Pipes',
UserManagementDataProviders: 'Pipes',

// User Management MFA -> MultiFactorAuth
Expand Down