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
13 changes: 8 additions & 5 deletions e2e/connector-loading.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ TMPDIR_BASE=$(mktemp -d)
cleanup() {
rm -rf "$TMPDIR_BASE"
rm -f "$REPO_ROOT"/stripe-sync-protocol-*.tgz
rm -f "$REPO_ROOT"/stripe-sync-openapi-*.tgz
rm -f "$REPO_ROOT"/stripe-sync-engine-*.tgz
rm -f "$REPO_ROOT"/stripe-sync-source-stripe-*.tgz
rm -f "$REPO_ROOT"/stripe-sync-destination-postgres-*.tgz
Expand All @@ -44,6 +45,7 @@ echo ""
echo "--- Step 1: Packing packages ---"

PROTOCOL_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-protocol pack 2>/dev/null | tail -1)
OPENAPI_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-openapi pack 2>/dev/null | tail -1)
ENGINE_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-engine pack 2>/dev/null | tail -1)
SOURCE_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-source-stripe pack 2>/dev/null | tail -1)
DEST_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-destination-postgres pack 2>/dev/null | tail -1)
Expand All @@ -52,7 +54,7 @@ STATE_PG_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-state-postgres pack
UTIL_PG_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-util-postgres pack 2>/dev/null | tail -1)
TSCLI_TGZ=$(cd "$REPO_ROOT" && pnpm --filter @stripe/sync-ts-cli pack 2>/dev/null | tail -1)

for tgz in "$PROTOCOL_TGZ" "$ENGINE_TGZ" "$SOURCE_TGZ" "$DEST_TGZ" "$DEST_SHEETS_TGZ" \
for tgz in "$PROTOCOL_TGZ" "$OPENAPI_TGZ" "$ENGINE_TGZ" "$SOURCE_TGZ" "$DEST_TGZ" "$DEST_SHEETS_TGZ" \
"$STATE_PG_TGZ" "$UTIL_PG_TGZ" "$TSCLI_TGZ"; do
if [ ! -f "$tgz" ]; then
echo "FAIL: tarball not found: $tgz"
Expand Down Expand Up @@ -84,6 +86,7 @@ cat > package.json <<EOF
"pnpm": {
"overrides": {
"@stripe/sync-protocol": "$PROTOCOL_TGZ",
"@stripe/sync-openapi": "$OPENAPI_TGZ",
"@stripe/sync-engine": "$ENGINE_TGZ",
"@stripe/sync-source-stripe": "$SOURCE_TGZ",
"@stripe/sync-destination-postgres": "$DEST_TGZ",
Expand All @@ -96,7 +99,7 @@ cat > package.json <<EOF
}
EOF

pnpm add "$PROTOCOL_TGZ" "$ENGINE_TGZ" "$SOURCE_TGZ" "$DEST_TGZ" "$DEST_SHEETS_TGZ" \
pnpm add "$PROTOCOL_TGZ" "$OPENAPI_TGZ" "$ENGINE_TGZ" "$SOURCE_TGZ" "$DEST_TGZ" "$DEST_SHEETS_TGZ" \
"$STATE_PG_TGZ" "$UTIL_PG_TGZ" "$TSCLI_TGZ" \
2>&1 | tail -5
echo ""
Expand Down Expand Up @@ -189,14 +192,14 @@ echo ""
# ---------------------------------------------------------------------------
echo "--- Step 8: unknown connector name → not found ---"
UNKNOWN_PARAMS='{"source_name":"nonexistent-xyz","source_config":{},"destination_name":"nonexistent-xyz","destination_config":{},"streams":[{"name":"x"}]}'
STEP8_OUTPUT=$(npx sync-engine check \
unknown_output=$(npx sync-engine check \
--x-sync-params "$UNKNOWN_PARAMS" \
2>&1 || true)
if echo "$STEP8_OUTPUT" | grep -qi "not found"; then
if echo "$unknown_output" | grep -qi "not found"; then
echo " PASS: unknown connector correctly reports 'not found'"
else
echo " FAIL: unknown connector did not report 'not found'"
echo " Output: $STEP8_OUTPUT"
echo " Output: $unknown_output"
exit 1
fi
echo ""
Expand Down
196 changes: 196 additions & 0 deletions packages/openapi/__tests__/fixtures/minimalSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import type { OpenApiSpec, OpenApiPathItem } from '../../types'

function listPath(
schemaRef: string,
opts: { supportsCreatedFilter?: boolean; supportsLimit?: boolean } = {}
): OpenApiPathItem {
const parameters: { name: string; in: string }[] = []
if (opts.supportsCreatedFilter) {
parameters.push({ name: 'created', in: 'query' })
}
if (opts.supportsLimit !== false) {
parameters.push({ name: 'limit', in: 'query' })
}
return {
get: {
parameters,
responses: {
'200': {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
object: { type: 'string', enum: ['list'] },
data: { type: 'array', items: { $ref: `#/components/schemas/${schemaRef}` } },
has_more: { type: 'boolean' },
url: { type: 'string' },
},
},
},
},
},
},
},
}
}

function v2ListPath(schemaRef: string): OpenApiPathItem {
return {
get: {
responses: {
'200': {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: { type: 'array', items: { $ref: `#/components/schemas/${schemaRef}` } },
next_page_url: { type: 'string', nullable: true },
previous_page_url: { type: 'string', nullable: true },
},
},
},
},
},
},
},
}
}

export const minimalStripeOpenApiSpec: OpenApiSpec = {
openapi: '3.0.0',
info: {
version: '2020-08-27',
},
paths: {
'/v1/customers': listPath('customer', { supportsCreatedFilter: true }),
'/v1/plans': listPath('plan', { supportsCreatedFilter: true }),
'/v1/prices': listPath('price', { supportsCreatedFilter: true }),
'/v1/products': listPath('product', { supportsCreatedFilter: true }),
'/v1/subscription_items': listPath('subscription_item'),
'/v1/checkout/sessions': listPath('checkout_session', { supportsCreatedFilter: true }),
'/v1/radar/early_fraud_warnings': listPath('early_fraud_warning', {
supportsCreatedFilter: true,
}),
'/v1/entitlements/active_entitlements': listPath('active_entitlement'),
'/v1/entitlements/features': listPath('entitlements_feature'),
'/v2/core/accounts': v2ListPath('v2_core_account'),
'/v2/core/event_destinations': v2ListPath('v2_core_event_destination'),
},
components: {
schemas: {
customer: {
'x-resourceId': 'customer',
oneOf: [
{
type: 'object',
properties: {
id: { type: 'string' },
object: { type: 'string' },
created: { type: 'integer' },
},
},
{
type: 'object',
properties: {
id: { type: 'string' },
deleted: { type: 'boolean' },
},
},
],
},
plan: {
'x-resourceId': 'plan',
type: 'object',
properties: {
id: { type: 'string' },
active: { type: 'boolean' },
amount: { type: 'integer' },
},
},
price: {
'x-resourceId': 'price',
type: 'object',
properties: {
id: { type: 'string' },
product: { type: 'string' },
unit_amount: { type: 'integer' },
metadata: { type: 'object', additionalProperties: true },
},
},
product: {
'x-resourceId': 'product',
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
subscription_item: {
'x-resourceId': 'subscription_item',
type: 'object',
properties: {
id: { type: 'string' },
deleted: { type: 'boolean' },
subscription: { type: 'string' },
quantity: { type: 'integer' },
},
},
checkout_session: {
'x-resourceId': 'checkout.session',
type: 'object',
properties: {
id: { type: 'string' },
amount_total: { type: 'integer' },
customer: { type: 'string', nullable: true },
},
},
early_fraud_warning: {
'x-resourceId': 'radar.early_fraud_warning',
type: 'object',
properties: {
id: { type: 'string' },
charge: { type: 'string' },
},
},
active_entitlement: {
'x-resourceId': 'entitlements.active_entitlement',
type: 'object',
properties: {
id: { type: 'string' },
customer: { type: 'string' },
feature: { type: 'string' },
},
},
entitlements_feature: {
'x-resourceId': 'entitlements.feature',
type: 'object',
properties: {
id: { type: 'string' },
lookup_key: { type: 'string' },
},
},
v2_core_account: {
'x-resourceId': 'v2.core.account',
type: 'object',
properties: {
id: { type: 'string' },
display_name: { type: 'string' },
contact_email: { type: 'string', nullable: true },
created: { type: 'string', format: 'date-time' },
},
},
v2_core_event_destination: {
'x-resourceId': 'v2.core.event_destination',
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
enabled_events: { type: 'array', items: { type: 'string' } },
livemode: { type: 'boolean' },
},
},
},
},
}
90 changes: 90 additions & 0 deletions packages/openapi/__tests__/listFnResolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import { discoverListEndpoints } from '../listFnResolver'
import { minimalStripeOpenApiSpec } from './fixtures/minimalSpec'

describe('discoverListEndpoints', () => {
it('maps table names to their API paths', () => {
const endpoints = discoverListEndpoints(minimalStripeOpenApiSpec)

expect(endpoints.get('customers')).toEqual({
tableName: 'customers',
resourceId: 'customer',
apiPath: '/v1/customers',
supportsCreatedFilter: true,
supportsLimit: true,
})
expect(endpoints.get('checkout_sessions')).toEqual({
tableName: 'checkout_sessions',
resourceId: 'checkout.session',
apiPath: '/v1/checkout/sessions',
supportsCreatedFilter: true,
supportsLimit: true,
})
expect(endpoints.get('early_fraud_warnings')).toEqual({
tableName: 'early_fraud_warnings',
resourceId: 'radar.early_fraud_warning',
apiPath: '/v1/radar/early_fraud_warnings',
supportsCreatedFilter: true,
supportsLimit: true,
})
})

it('discovers v2 list endpoints using next_page_url format', () => {
const endpoints = discoverListEndpoints(minimalStripeOpenApiSpec)

expect(endpoints.get('v2_core_accounts')).toEqual({
tableName: 'v2_core_accounts',
resourceId: 'v2.core.account',
apiPath: '/v2/core/accounts',
supportsCreatedFilter: false,
supportsLimit: false,
})
expect(endpoints.get('v2_core_event_destinations')).toEqual({
tableName: 'v2_core_event_destinations',
resourceId: 'v2.core.event_destination',
apiPath: '/v2/core/event_destinations',
supportsCreatedFilter: false,
supportsLimit: false,
})
})

it('skips paths with path parameters', () => {
const spec = {
...minimalStripeOpenApiSpec,
paths: {
...minimalStripeOpenApiSpec.paths,
'/v1/customers/{customer}/sources': {
get: {
responses: {
'200': {
content: {
'application/json': {
schema: {
type: 'object' as const,
properties: {
object: { type: 'string' as const, enum: ['list'] },
data: {
type: 'array' as const,
items: { $ref: '#/components/schemas/customer' },
},
has_more: { type: 'boolean' as const },
},
},
},
},
},
},
},
},
},
}
const endpoints = discoverListEndpoints(spec)
const paths = Array.from(endpoints.values()).map((e) => e.apiPath)
expect(paths).not.toContain('/v1/customers/{customer}/sources')
})

it('returns empty map when spec has no paths', () => {
const endpoints = discoverListEndpoints({ openapi: '3.0.0' })
expect(endpoints.size).toBe(0)
})
})
Loading