diff --git a/CHANGELOG.md b/CHANGELOG.md index b51efcc..cc479b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,45 @@ project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [2.7.0] - 2026-09-19 + +### Added +- `client.photoMockups` and `client.psdMockups`, the family names of the two + mockup resources. `client.photoMockups` is the object `client.ai` was; + `client.psdMockups` is the object `client.mockups` was. +- Family type names beside the earlier ones: `PhotoMockup`, + `PhotoMockupDetails`, `PhotoMockupQuad`, `PhotoMockupSurface`, + `PhotoMockupPrintAreaInput`, `PhotoMockupListResult`, + `ListPhotoMockupsParams`, `CreatePhotoMockupParams`, + `CreatePhotoMockupResult`, `WaitForPhotoMockupOptions`, + `UpdatePhotoMockupPrintAreasResult`, `PhotoMockupRenderParams`, + `PhotoMockupRenderResult`, `PhotoMockupPrintArea`, `PhotoMockupPrintFile`, + `PhotoMockupAdjustments` and `PhotoMockupPlacement`. Each is the same shape + as the name it stands beside; the earlier names stay exported. + +### Changed +- Requests travel on the family paths: `client.photoMockups` posts to + `/api/v1/photo-mockups` (was `/api/v1/sudoai/2d-mockups`) and + `client.psdMockups` to `/api/v1/psd-mockups` (was `/api/v1/mockups`). The + API keeps serving the earlier paths, so a project pinned to 2.6.x is not + affected. +- **Job kinds.** Because requests now travel on the family path, a job + submitted through this SDK is named by its family: an async + `photoMockups.create()` resolves with `kind: 'photo_mockup_create'` and an + async `photoMockups.render()` with `kind: 'photo_mockup_render'`, where 2.6.x + returned `'2d_create'` / `'2d_render'`. Code that compares `job.kind` against + a `2d_*` literal must compare against the family name (or both). `JobKind` + keeps both spellings, `jobs.list({ kind })` with either returns both, and + webhook event names are untouched: an endpoint keeps the spelling it is + pinned to (`eventNaming`). + +### Deprecated +- `client.ai` and `client.mockups`. Both keep working and return the very same + objects as `client.photoMockups` / `client.psdMockups`; the first access to + each emits a one-time `DeprecationWarning` naming the replacement. + +Nothing was removed. + ## [2.6.0] - 2026-09-18 ### Added diff --git a/README.md b/README.md index fce3aa8..d5ddf9e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SudoMock Node.js SDK -Official Node.js/TypeScript SDK for the [SudoMock API](https://sudomock.com/docs). Generate product mockups from Photoshop PSD files or use AI-powered rendering without any PSD. +Official Node.js/TypeScript SDK for the [SudoMock API](https://sudomock.com/docs). Generate product mockups from Photoshop PSD files, or from a product photo without any PSD. ## Installation @@ -19,7 +19,7 @@ const client = new SudoMock('sm_your_api_key') // or set SUDOMOCK_API_KEY env var and call: new SudoMock() // List your mockups -const { mockups, total } = await client.mockups.list({ limit: 10 }) +const { mockups, total } = await client.psdMockups.list({ limit: 10 }) console.log(`Found ${total} mockups`) // Render a mockup with artwork @@ -52,11 +52,14 @@ The API key can be passed as the first argument or via the `SUDOMOCK_API_KEY` en Every request identifies itself with `X-SudoMock-Client: node-sdk/` (and the same value as `User-Agent` where the runtime allows it), so you can pick the SDK's traffic out of your own logs and proxy rules. -### Mockups +`client.ai` and `client.mockups` are the earlier names of `client.photoMockups` +and `client.psdMockups`. They return the same objects and warn once per process. + +### PSD Mockups (`client.psdMockups`) ```typescript // List mockups with pagination and filtering -const result = await client.mockups.list({ +const result = await client.psdMockups.list({ limit: 20, offset: 0, name: 'shirt', // case-insensitive contains @@ -65,17 +68,17 @@ const result = await client.mockups.list({ }) // Get a single mockup -const mockup = await client.mockups.get('uuid') +const mockup = await client.psdMockups.get('uuid') console.log(mockup.smartObjects) // Update mockup name -const updated = await client.mockups.update('uuid', { name: 'New Name' }) +const updated = await client.psdMockups.update('uuid', { name: 'New Name' }) // Delete a mockup -await client.mockups.delete('uuid') +await client.psdMockups.delete('uuid') ``` -> **Bulk delete all mockups** (`DELETE /api/v1/mockups/all`) requires a +> **Bulk delete all mockups** (`DELETE /api/v1/psd-mockups/all`) requires a > dashboard Bearer token and is **not** callable with an API key (the API > returns 403). It is therefore intentionally not exposed by this SDK -- use > the dashboard. @@ -119,7 +122,7 @@ outputs. `smartObjects` is optional for text-only renders, and `fit` defaults to `'overflow'`. ```typescript -const mockup = await client.mockups.get('mockup-uuid') +const mockup = await client.psdMockups.get('mockup-uuid') const nameLayer = mockup.textLayers.find((layer) => layer.name === 'Customer Name') if (!nameLayer?.isEditable) throw new Error('Customer Name is not editable') @@ -174,8 +177,9 @@ List and page your jobs (keyset pagination, newest first). Filter by `kind` (`render` | `video` | `upload` | `2d_create` | `2d_render` | `photo_mockup_create` | `photo_mockup_render`) and/or `mockupUuid`. A photo-mockup job is spelled `photo_mockup_*` when submitted on -`/api/v1/photo-mockups` and `2d_*` when submitted through `client.ai`; filtering -by either spelling returns both: +`/api/v1/photo-mockups` (where `client.photoMockups` posts since 2.7.0) and +`2d_*` when submitted on the earlier `/api/v1/sudoai/2d-mockups` path; +filtering by either spelling returns both: ```typescript const { jobs, nextCursor } = await client.jobs.list({ kind: 'video', limit: 50 }) @@ -214,15 +218,15 @@ const job = await client.renders.createVideo({ }) ``` -### 2D Mockups (`client.ai`) +### Photo Mockups (`client.photoMockups`) Render artwork onto an existing 2D mockup (no PSD template) and manage your 2D -mockup catalog. `client.ai.render` posts to -`/api/v1/sudoai/2d-mockups/{mockupId}/render` (the mockup id lives in the path) +mockup catalog. `client.photoMockups.render` posts to +`/api/v1/photo-mockups/{mockupId}/render` (the mockup id lives in the path) and costs **5 credits** per call. Each print area must supply `artworkUrl` OR `color`. Use `uuid` for a saved print area -- a bounded zone drawn on the product -- or `surfaceUuid` for a product surface, both returned by -`client.ai.get()`. A product can have both, and they are separate targets. +`client.photoMockups.get()`. A product can have both, and they are separate targets. Placement follows the target, and sizing has one answer per request: a surface takes a `coverage` percentage or an explicit `width` + `height`, a print area takes a `fit` or an explicit `width` + `height`. Anchoring -- `position`, @@ -232,7 +236,7 @@ handing one the other's dial does not compile. #### Render an existing 2D mockup ```typescript -const result = await client.ai.render({ +const result = await client.photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [{ uuid: 'print-area-uuid', @@ -249,13 +253,13 @@ console.log(result.printFiles[0].exportFormat) // 'webp' ``` Prefer to render in the background? Pass `isAsync: true` and `render()` resolves -with a `Job` of kind `'2d_render'` (`202 Accepted`) you await with +with a `Job` of kind `'photo_mockup_render'` (`202 Accepted`) you await with `jobs.waitForJob`. A `photo_mockup_render.succeeded` / `photo_mockup_render.failed` webhook also fires (`2d_render.succeeded` / `2d_render.failed` on an endpoint that keeps the legacy event names). ```typescript -const job = await client.ai.render({ +const job = await client.photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [{ uuid: 'print-area-uuid', @@ -277,7 +281,7 @@ Create a reusable 2D mockup, then render artwork. By default creation is **25 credits** are refunded automatically. ```typescript -const mockup = await client.ai.create({ +const mockup = await client.photoMockups.create({ sourceUrl: 'https://example.com/product.jpg', name: 'Front view', idempotencyKey: 'front-view-v1', @@ -286,7 +290,7 @@ const mockup = await client.ai.create({ const printArea = mockup.quads[0] if (!printArea) throw new Error('No print area available') -const result = await client.ai.render({ +const result = await client.photoMockups.render({ mockupId: mockup.mockupId, printAreas: [{ uuid: printArea.printAreaId, @@ -297,7 +301,7 @@ console.log(result.url) const fullSurface = mockup.surfaces[0] if (fullSurface) { - await client.ai.render({ + await client.photoMockups.render({ mockupId: mockup.mockupId, printAreas: [{ surfaceUuid: fullSurface.surfaceUuid, @@ -311,11 +315,11 @@ Prefer to create in the background? Pass `isAsync: true` and `create()` resolves with a `Job` you await with `waitForReady`: ```typescript -const job = await client.ai.create({ +const job = await client.photoMockups.create({ sourceUrl: 'https://example.com/product.jpg', isAsync: true, }) -const mockup = await client.ai.waitForReady(job, { intervalMs: 2_000 }) +const mockup = await client.photoMockups.waitForReady(job, { intervalMs: 2_000 }) ``` Update all print areas on a ready mockup with up to 8 four-point quads (each @@ -324,7 +328,7 @@ the API has verified every product surface as printable; each of those is then a render target in its own right: ```typescript -const updated = await client.ai.updatePrintAreas('mockup-uuid', [{ +const updated = await client.photoMockups.updatePrintAreas('mockup-uuid', [{ points: [[100, 100], [900, 100], [900, 900], [100, 900]], name: 'Front', }]) @@ -334,13 +338,13 @@ console.log(updated.printAreas) Manage the 2D-mockup catalog: ```typescript -const { mockups, total } = await client.ai.list({ +const { mockups, total } = await client.photoMockups.list({ limit: 50, customizableOnly: true, }) console.log(mockups[0]?.customizable) -const mockup = await client.ai.get('mockup-uuid') -await client.ai.delete('mockup-uuid') +const mockup = await client.photoMockups.get('mockup-uuid') +await client.photoMockups.delete('mockup-uuid') ``` ### Background Removal (`client.images`) @@ -382,7 +386,7 @@ await client.renders.create({ }) // 2D render -await client.ai.render({ +await client.photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [{ uuid: 'print-area-uuid', diff --git a/package-lock.json b/package-lock.json index d4ba912..069cef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sudomock", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sudomock", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "devDependencies": { "@types/node": "^22.19.15", diff --git a/package.json b/package.json index 5369ba5..014a690 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sudomock", - "version": "2.6.0", + "version": "2.7.0", "description": "Official Node.js/TypeScript SDK for the SudoMock API - mockup generation from PSD files", "type": "module", "main": "./dist/index.cjs", diff --git a/src/index.ts b/src/index.ts index c899cf3..48d7cdf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { deprecate } from 'node:util' import { HttpClient } from './client' import { SudoMockError } from './errors' import { MockupsResource } from './resources/mockups' @@ -15,6 +16,17 @@ const DEFAULT_BASE_URL = 'https://api.sudomock.com' const DEFAULT_TIMEOUT = 30_000 const DEFAULT_MAX_RETRIES = 2 +// Earlier accessor names. Each warns once per process, then behaves exactly +// like the name it stands for. +const warnAi = deprecate( + () => {}, + 'client.ai is deprecated, use client.photoMockups (the same object).', +) +const warnMockups = deprecate( + () => {}, + 'client.mockups is deprecated, use client.psdMockups (the same object).', +) + /** * SudoMock API client. * @@ -25,7 +37,7 @@ const DEFAULT_MAX_RETRIES = 2 * const client = new SudoMock('sm_xxx') * * // List mockups - * const { mockups } = await client.mockups.list() + * const { mockups } = await client.psdMockups.list() * * // Render * const render = await client.renders.create({ @@ -39,12 +51,12 @@ const DEFAULT_MAX_RETRIES = 2 * ``` */ class SudoMock { - /** Mockup CRUD operations */ - readonly mockups: MockupsResource + /** PSD mockups: list, inspect, rename and delete uploaded templates */ + readonly psdMockups: MockupsResource /** Render mockups with artwork */ readonly renders: RendersResource - /** Create and render reusable 2D mockups */ - readonly ai: AIResource + /** Photo mockups: create reusable mockups from product photos and render onto them */ + readonly photoMockups: AIResource /** Standalone image operations (background removal) */ readonly images: ImagesResource /** Upload PSD files */ @@ -53,7 +65,7 @@ class SudoMock { readonly account: AccountResource /** Studio session management */ readonly studio: StudioResource - /** Poll async render, video, upload, and 2D-creation jobs */ + /** Poll async render, video, upload, and photo-mockup jobs */ readonly jobs: JobsResource /** Manage webhook endpoints and their deliveries */ readonly webhooks: WebhooksResource @@ -74,9 +86,9 @@ class SudoMock { maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES, }) - this.mockups = new MockupsResource(client) + this.psdMockups = new MockupsResource(client) this.renders = new RendersResource(client) - this.ai = new AIResource(client) + this.photoMockups = new AIResource(client) this.images = new ImagesResource(client) this.uploads = new UploadsResource(client) this.account = new AccountResource(client) @@ -84,6 +96,18 @@ class SudoMock { this.jobs = new JobsResource(client) this.webhooks = new WebhooksResource(client) } + + /** @deprecated Earlier name of {@link SudoMock.psdMockups}. The same object; warns once per process. */ + get mockups(): MockupsResource { + warnMockups() + return this.psdMockups + } + + /** @deprecated Earlier name of {@link SudoMock.photoMockups}. The same object; warns once per process. */ + get ai(): AIResource { + warnAi() + return this.photoMockups + } } // Default export for convenient `import SudoMock from 'sudomock'` @@ -154,6 +178,23 @@ export type { TwoDMockupQuad, TwoDMockupListResult, List2dMockupsParams, + PhotoMockup, + PhotoMockupDetails, + PhotoMockupQuad, + PhotoMockupSurface, + PhotoMockupPrintAreaInput, + PhotoMockupListResult, + ListPhotoMockupsParams, + CreatePhotoMockupParams, + CreatePhotoMockupResult, + WaitForPhotoMockupOptions, + UpdatePhotoMockupPrintAreasResult, + PhotoMockupRenderParams, + PhotoMockupRenderResult, + PhotoMockupPrintArea, + PhotoMockupPrintFile, + PhotoMockupAdjustments, + PhotoMockupPlacement, RemoveBackgroundParams, RemoveBackgroundResult, UploadParams, diff --git a/src/resources/ai.ts b/src/resources/ai.ts index a5660c6..f077f9d 100644 --- a/src/resources/ai.ts +++ b/src/resources/ai.ts @@ -139,10 +139,11 @@ function wirePlacement( } /** - * SudoAI 2D mockups (`client.ai`). + * Photo mockups (`client.photoMockups`). * - * Create and render reusable 2D mockups, and manage your 2D-mockup catalog. - * Creation costs 25 credits. Rendering costs 5 credits per call. + * Create reusable mockups from product photos, render artwork onto them, and + * manage that catalog. Creation costs 25 credits. Rendering costs 5 credits + * per call. */ export class AIResource { constructor(private readonly client: HttpClient) {} @@ -162,7 +163,7 @@ export class AIResource { * * @example Synchronous (default) * ```ts - * const mockup = await client.ai.create({ + * const mockup = await client.photoMockups.create({ * sourceUrl: 'https://example.com/product.jpg', * name: 'Front view', * idempotencyKey: 'front-view-v1', @@ -172,11 +173,11 @@ export class AIResource { * * @example Asynchronous * ```ts - * const job = await client.ai.create({ + * const job = await client.photoMockups.create({ * sourceUrl: 'https://example.com/product.jpg', * isAsync: true, * }) - * const mockup = await client.ai.waitForReady(job) + * const mockup = await client.photoMockups.waitForReady(job) * ``` */ create(params: Create2DMockupParams & { isAsync: true }): Promise @@ -210,7 +211,7 @@ export class AIResource { TwoDMockupDetails | Job >({ method: 'POST', - path: '/api/v1/sudoai/2d-mockups', + path: '/api/v1/photo-mockups', body, headers: { 'Idempotency-Key': idempotencyKey }, }) @@ -268,7 +269,7 @@ export class AIResource { /** * Render artwork (or a color) onto an existing 2D mockup. * - * Targets `POST /api/v1/sudoai/2d-mockups/{mockupId}/render` (the mockup id + * Targets `POST /api/v1/photo-mockups/{mockupId}/render` (the mockup id * lives in the path). Target a saved print area with `uuid` -- a bounded zone * drawn on the product, which takes a `fit` or an explicit box -- or a product * surface with `surfaceUuid`, which takes a `coverage` percentage. A product @@ -279,13 +280,14 @@ export class AIResource { * {@link AIRenderResult} (the rendered `printFiles` plus a `renderUuid` you * can use to correlate the render with webhook / transaction records). Pass * `isAsync: true` to enqueue the render instead: the API responds with - * HTTP 202 and this method resolves with a {@link Job} of kind `'2d_render'` + * HTTP 202 and this method resolves with a {@link Job} of kind + * `'photo_mockup_render'` * you await with `client.jobs.waitForJob(job.jobId)` (or poll via * `client.jobs`). * * @example Synchronous (default) * ```ts - * const result = await client.ai.render({ + * const result = await client.photoMockups.render({ * mockupId: 'mockup-uuid', * printAreas: [{ * uuid: 'print-area-uuid', @@ -297,7 +299,7 @@ export class AIResource { * * @example Asynchronous * ```ts - * const job = await client.ai.render({ + * const job = await client.photoMockups.render({ * mockupId: 'mockup-uuid', * printAreas: [{ uuid: 'print-area-uuid', artworkUrl: '...' }], * isAsync: true, @@ -350,7 +352,7 @@ export class AIResource { AIRenderResult | Job >({ method: 'POST', - path: `/api/v1/sudoai/2d-mockups/${params.mockupId}/render`, + path: `/api/v1/photo-mockups/${params.mockupId}/render`, body, timeout: AI_RENDER_TIMEOUT, }) @@ -382,7 +384,7 @@ export class AIResource { * * @example * ```ts - * const { mockups, total } = await client.ai.list({ limit: 50 }) + * const { mockups, total } = await client.photoMockups.list({ limit: 50 }) * ``` */ async list( @@ -398,7 +400,7 @@ export class AIResource { offset?: number }>({ method: 'GET', - path: '/api/v1/sudoai/2d-mockups', + path: '/api/v1/photo-mockups', query: { limit: params.limit, offset: params.offset, @@ -421,7 +423,7 @@ export class AIResource { async get(mockupId: string): Promise { const mockup = await this.client.request({ method: 'GET', - path: `/api/v1/sudoai/2d-mockups/${mockupId}`, + path: `/api/v1/photo-mockups/${mockupId}`, }) return publicMockupDetails(mockup) } @@ -442,7 +444,7 @@ export class AIResource { const result = await this.client.request({ method: 'PUT', - path: `/api/v1/sudoai/2d-mockups/${mockupId}/print-areas`, + path: `/api/v1/photo-mockups/${mockupId}/print-areas`, body: { printAreas }, }) return { @@ -457,7 +459,7 @@ export class AIResource { async delete(mockupId: string): Promise { await this.client.request({ method: 'DELETE', - path: `/api/v1/sudoai/2d-mockups/${mockupId}`, + path: `/api/v1/photo-mockups/${mockupId}`, }) } } diff --git a/src/resources/mockups.ts b/src/resources/mockups.ts index 7f08995..e04d47f 100644 --- a/src/resources/mockups.ts +++ b/src/resources/mockups.ts @@ -82,13 +82,13 @@ export class MockupsResource { * * @example * ```ts - * const { mockups, total } = await client.mockups.list({ limit: 10 }) + * const { mockups, total } = await client.psdMockups.list({ limit: 10 }) * ``` */ async list(params: ListMockupsParams = {}): Promise { const result = await this.client.request({ method: 'GET', - path: '/api/v1/mockups', + path: '/api/v1/psd-mockups', query: { limit: params.limit, offset: params.offset, @@ -112,14 +112,14 @@ export class MockupsResource { * * @example * ```ts - * const mockup = await client.mockups.get('uuid') + * const mockup = await client.psdMockups.get('uuid') * console.log(mockup.smartObjects) * ``` */ async get(uuid: string): Promise { const mockup = await this.client.request({ method: 'GET', - path: `/api/v1/mockups/${uuid}`, + path: `/api/v1/psd-mockups/${uuid}`, }) return toPublicMockup(mockup) } @@ -129,13 +129,13 @@ export class MockupsResource { * * @example * ```ts - * const updated = await client.mockups.update('uuid', { name: 'New Name' }) + * const updated = await client.psdMockups.update('uuid', { name: 'New Name' }) * ``` */ async update(uuid: string, params: { name: string }): Promise { const mockup = await this.client.request({ method: 'PATCH', - path: `/api/v1/mockups/${uuid}`, + path: `/api/v1/psd-mockups/${uuid}`, body: params, }) return toPublicMockup(mockup) @@ -146,13 +146,13 @@ export class MockupsResource { * * @example * ```ts - * await client.mockups.delete('uuid') + * await client.psdMockups.delete('uuid') * ``` */ async delete(uuid: string): Promise { await this.client.request({ method: 'DELETE', - path: `/api/v1/mockups/${uuid}`, + path: `/api/v1/psd-mockups/${uuid}`, }) } } diff --git a/src/types.ts b/src/types.ts index 4fe74a0..8384de1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -339,7 +339,7 @@ export interface RenderResult { } // --------------------------------------------------------------------------- -// SudoAI 2D Mockups (client.ai) +// Photo Mockups (client.photoMockups) // --------------------------------------------------------------------------- /** Image adjustments applied to a 2D-mockup print area. */ @@ -519,7 +519,7 @@ export interface AIRenderParams { * Submit the 2D render asynchronously. * * When `true`, the API enqueues the render and immediately returns a - * {@link Job} of kind `'2d_render'` (HTTP 202) instead of blocking until the + * {@link Job} of kind `'photo_mockup_render'` (HTTP 202) instead of blocking until the * render completes. Poll the job with `client.jobs.retrieve(job.jobId)` or * `client.jobs.waitForJob(job.jobId)`; a `photo_mockup_render.succeeded` / * `photo_mockup_render.failed` webhook also fires (`2d_render.succeeded` / @@ -566,7 +566,7 @@ export type Create2DMockupParams = { * * When `true`, the API enqueues creation and immediately returns a * {@link Job} (HTTP 202) instead of blocking until the mockup is ready. Pass - * the job to `client.ai.waitForReady()` (or poll via `client.jobs`). + * the job to `client.photoMockups.waitForReady()` (or poll via `client.jobs`). * * Default: `false` (synchronous, returns the ready {@link TwoDMockupDetails}). */ @@ -579,13 +579,13 @@ export type Create2DMockupParams = { /** Accepted 2D-mockup creation job. */ export interface Create2DMockupResult { jobId: string - /** `'2d_create'` from `client.ai`; `'photo_mockup_create'` when submitted on `/api/v1/photo-mockups`. */ + /** `'photo_mockup_create'` from `client.photoMockups` (2.7.0 and later); `'2d_create'` from this SDK up to 2.6.x. */ kind: '2d_create' | 'photo_mockup_create' status: 'queued' statusUrl: string } -/** Options for `client.ai.waitForReady()`. */ +/** Options for `client.photoMockups.waitForReady()`. */ export interface WaitFor2DMockupOptions { /** Milliseconds between polls (default: 2000). */ intervalMs?: number @@ -632,13 +632,13 @@ export interface TwoDPrintAreaInput { name?: string } -/** Updated geometry returned by `client.ai.updatePrintAreas()`. */ +/** Updated geometry returned by `client.photoMockups.updatePrintAreas()`. */ export interface Update2DPrintAreasResult { mockupId: string printAreas: TwoDMockupQuad[] } -/** A 2D mockup as returned by `client.ai.get()` / `client.ai.list()`. */ +/** A 2D mockup as returned by `client.photoMockups.get()` / `client.photoMockups.list()`. */ export interface TwoDMockup { mockupId: string name: string @@ -658,14 +658,14 @@ export interface TwoDMockup { updatedAt: string } -/** Full 2D mockup returned by `client.ai.get()` and `waitForReady()`. */ +/** Full 2D mockup returned by `client.photoMockups.get()` and `waitForReady()`. */ export interface TwoDMockupDetails extends TwoDMockup { quads: TwoDMockupQuad[] /** Every printable product in the photo, each a render target on its own. */ surfaces: TwoDFullSurface[] } -/** Pagination params for `client.ai.list()`. */ +/** Pagination params for `client.photoMockups.list()`. */ export interface List2dMockupsParams { /** Number of mockups to return (1-100, default: 20). */ limit?: number @@ -676,7 +676,7 @@ export interface List2dMockupsParams { } /** - * An offset-paginated page of 2D mockups (`client.ai.list()`). + * An offset-paginated page of 2D mockups (`client.photoMockups.list()`). * * The API returns the pagination metadata (`total` / `limit` / `offset`) * as siblings of the `data` array; this surfaces it so callers can drive @@ -759,17 +759,41 @@ export interface UploadResult { } // --------------------------------------------------------------------------- -// Jobs (async renders / videos / uploads / 2D creation) +// Photo-mockup family names. Each is the same shape as the name it stands +// beside; both spellings stay exported. +// --------------------------------------------------------------------------- + +export type PhotoMockup = TwoDMockup +export type PhotoMockupDetails = TwoDMockupDetails +export type PhotoMockupQuad = TwoDMockupQuad +export type PhotoMockupSurface = TwoDFullSurface +export type PhotoMockupPrintAreaInput = TwoDPrintAreaInput +export type PhotoMockupListResult = TwoDMockupListResult +export type ListPhotoMockupsParams = List2dMockupsParams +export type CreatePhotoMockupParams = Create2DMockupParams +export type CreatePhotoMockupResult = Create2DMockupResult +export type WaitForPhotoMockupOptions = WaitFor2DMockupOptions +export type UpdatePhotoMockupPrintAreasResult = Update2DPrintAreasResult +export type PhotoMockupRenderParams = AIRenderParams +export type PhotoMockupRenderResult = AIRenderResult +export type PhotoMockupPrintArea = AIPrintArea +export type PhotoMockupPrintFile = AIPrintFile +export type PhotoMockupAdjustments = AIAdjustments +export type PhotoMockupPlacement = AIPlacement + +// --------------------------------------------------------------------------- +// Jobs (async renders / videos / uploads / photo-mockup creation) // --------------------------------------------------------------------------- /** * The kind of work a job performs. * - * A photo-mockup job has two spellings: `'photo_mockup_create'` / - * `'photo_mockup_render'` when submitted on `/api/v1/photo-mockups`, and - * `'2d_create'` / `'2d_render'` when submitted on `/api/v1/sudoai/2d-mockups` - * (the path `client.ai` posts to). Filtering {@link JobsResource.list} by - * either spelling returns both. + * A photo-mockup job is named `'photo_mockup_create'` / `'photo_mockup_render'` + * when submitted on `/api/v1/photo-mockups`, the path `client.photoMockups` + * posts to since 2.7.0. A job submitted on the earlier + * `/api/v1/sudoai/2d-mockups` path (this SDK up to 2.6.x) is named + * `'2d_create'` / `'2d_render'`. Filtering {@link JobsResource.list} by either + * spelling returns both. */ export type JobKind = | 'render' diff --git a/tests/client.test.ts b/tests/client.test.ts index e2f3eb5..901866d 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -46,9 +46,9 @@ describe('SudoMock client', () => { it('exposes resource namespaces', () => { const client = createClient() - expect(client.mockups).toBeDefined() + expect(client.psdMockups).toBeDefined() expect(client.renders).toBeDefined() - expect(client.ai).toBeDefined() + expect(client.photoMockups).toBeDefined() expect(client.uploads).toBeDefined() expect(client.account).toBeDefined() expect(client.studio).toBeDefined() @@ -70,7 +70,7 @@ describe('client identity', () => { function captureListHeaders(): () => Headers | undefined { let captured: Headers | undefined server.use( - http.get(`${TEST_BASE_URL}/api/v1/mockups`, ({ request }) => { + http.get(`${TEST_BASE_URL}/api/v1/psd-mockups`, ({ request }) => { captured = request.headers return HttpResponse.json({ success: true, @@ -89,7 +89,7 @@ describe('client identity', () => { it('sends X-SudoMock-Client and User-Agent as node-sdk/ on every request', async () => { const headers = captureListHeaders() - await createClient().mockups.list() + await createClient().psdMockups.list() expect(headers()?.get('X-SudoMock-Client')).toBe(`node-sdk/${packageVersion}`) expect(headers()?.get('User-Agent')).toBe(`node-sdk/${packageVersion}`) @@ -108,7 +108,7 @@ describe('client identity', () => { try { const headers = captureListHeaders() - await createClient().mockups.list() + await createClient().psdMockups.list() expect(headers()?.get('X-SudoMock-Client')).toBe(`node-sdk/${packageVersion}`) expect(headers()?.get('User-Agent')).not.toBe(CLIENT_ID) @@ -128,7 +128,7 @@ describe('client identity', () => { await transport.request({ method: 'GET', - path: '/api/v1/mockups', + path: '/api/v1/psd-mockups', headers: { accept: 'application/json; charset=utf-8' }, }) @@ -144,12 +144,12 @@ describe('client identity', () => { describe('authentication', () => { it('throws AuthenticationError on 401', async () => { const client = new SudoMock('sm_bad_key', { baseUrl: TEST_BASE_URL }) - await expect(client.mockups.list()).rejects.toThrow(AuthenticationError) + await expect(client.psdMockups.list()).rejects.toThrow(AuthenticationError) }) it('does not expose engine diagnostics from API errors', async () => { server.use( - http.get(`${TEST_BASE_URL}/api/v1/mockups`, () => + http.get(`${TEST_BASE_URL}/api/v1/psd-mockups`, () => HttpResponse.json( { detail: 'Private model prompt failed for mask_uuid.', @@ -160,7 +160,7 @@ describe('authentication', () => { ), ) - const error = await createClient().mockups.list().catch((caught: unknown) => caught) + const error = await createClient().psdMockups.list().catch((caught: unknown) => caught) expect(error).toMatchObject({ message: 'The request could not be completed.', code: 'PROCESSING_FAILED', @@ -175,7 +175,7 @@ describe('authentication', () => { describe('rate limiting', () => { it('throws RateLimitError on 429 with retryAfter', async () => { server.use( - http.get(`${TEST_BASE_URL}/api/v1/mockups`, () => { + http.get(`${TEST_BASE_URL}/api/v1/psd-mockups`, () => { return HttpResponse.json( { detail: 'Too many requests' }, { @@ -188,7 +188,7 @@ describe('rate limiting', () => { const client = createClient({ maxRetries: 0 }) try { - await client.mockups.list() + await client.psdMockups.list() expect.fail('Should have thrown') } catch (err) { expect(err).toBeInstanceOf(RateLimitError) @@ -256,7 +256,7 @@ describe('retry', () => { it('does NOT retry on 400 (client error)', async () => { let attempt = 0 server.use( - http.get(`${TEST_BASE_URL}/api/v1/mockups/:uuid`, () => { + http.get(`${TEST_BASE_URL}/api/v1/psd-mockups/:uuid`, () => { attempt++ return HttpResponse.json( { detail: 'Bad request' }, @@ -267,7 +267,7 @@ describe('retry', () => { const client = createClient({ maxRetries: 3 }) await expect( - client.mockups.get('some-uuid'), + client.psdMockups.get('some-uuid'), ).rejects.toThrow(SudoMockError) expect(attempt).toBe(1) // no retries for client errors }) diff --git a/tests/mockups.test.ts b/tests/mockups.test.ts index 1655499..256e1c9 100644 --- a/tests/mockups.test.ts +++ b/tests/mockups.test.ts @@ -11,7 +11,7 @@ function createClient() { describe('mockups.list()', () => { it('returns mockups array with total count', async () => { const client = createClient() - const result = await client.mockups.list() + const result = await client.psdMockups.list() expect(result.mockups).toHaveLength(1) expect(result.total).toBe(1) @@ -21,7 +21,7 @@ describe('mockups.list()', () => { it('returns camelCase smart object fields', async () => { const client = createClient() - const result = await client.mockups.list() + const result = await client.psdMockups.list() const mockup = result.mockups[0]! expect(mockup.uuid).toBe(MOCK_MOCKUP.uuid) @@ -40,7 +40,7 @@ describe('mockups.list()', () => { it('passes query params correctly', async () => { let capturedUrl = '' server.use( - http.get(`${TEST_BASE_URL}/api/v1/mockups`, ({ request }) => { + http.get(`${TEST_BASE_URL}/api/v1/psd-mockups`, ({ request }) => { capturedUrl = request.url return HttpResponse.json({ success: true, @@ -50,7 +50,7 @@ describe('mockups.list()', () => { ) const client = createClient() - await client.mockups.list({ + await client.psdMockups.list({ limit: 10, offset: 5, name: 'shirt', @@ -70,7 +70,7 @@ describe('mockups.list()', () => { describe('mockups.get()', () => { it('returns a single mockup', async () => { const client = createClient() - const mockup = await client.mockups.get(MOCK_MOCKUP.uuid) + const mockup = await client.psdMockups.get(MOCK_MOCKUP.uuid) expect(mockup.uuid).toBe(MOCK_MOCKUP.uuid) expect(mockup.width).toBe(4000) @@ -81,7 +81,7 @@ describe('mockups.get()', () => { it('throws NotFoundError for missing mockup', async () => { const client = createClient() - await expect(client.mockups.get('not-found-uuid')).rejects.toThrow( + await expect(client.psdMockups.get('not-found-uuid')).rejects.toThrow( NotFoundError, ) }) @@ -90,7 +90,7 @@ describe('mockups.get()', () => { describe('mockups.update()', () => { it('updates mockup name', async () => { const client = createClient() - const result = await client.mockups.update(MOCK_MOCKUP.uuid, { + const result = await client.psdMockups.update(MOCK_MOCKUP.uuid, { name: 'Updated Mockup', }) @@ -102,6 +102,6 @@ describe('mockups.delete()', () => { it('deletes a mockup (204 response)', async () => { const client = createClient() // Should not throw - await client.mockups.delete(MOCK_MOCKUP.uuid) + await client.psdMockups.delete(MOCK_MOCKUP.uuid) }) }) diff --git a/tests/photo-mockup-names.test.ts b/tests/photo-mockup-names.test.ts index 06ba197..26480d1 100644 --- a/tests/photo-mockup-names.test.ts +++ b/tests/photo-mockup-names.test.ts @@ -55,7 +55,7 @@ describe('photo-mockup job kinds', () => { it('returns the 202 create job under its family kind', async () => { server.use( - http.post(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups`, () => + http.post(`${TEST_BASE_URL}/api/v1/photo-mockups`, () => HttpResponse.json( { job_id: JOB_ID, @@ -68,7 +68,7 @@ describe('photo-mockup job kinds', () => { ), ) - const job = await createClient().ai.create({ + const job = await createClient().photoMockups.create({ sourceUrl: 'https://example.com/product.jpg', isAsync: true, }) diff --git a/tests/photo-mockup-names.typecheck.ts b/tests/photo-mockup-names.typecheck.ts index 1752714..3270240 100644 --- a/tests/photo-mockup-names.typecheck.ts +++ b/tests/photo-mockup-names.typecheck.ts @@ -81,3 +81,68 @@ export const endpoint: WebhookEndpoint = { eventNaming: 'legacy', enabled: true, } + +// Family type names: each is the same shape as the name it stands beside, so a +// caller can switch spelling without a cast. +import type { + AIAdjustments, + AIPlacement, + AIPrintArea, + AIPrintFile, + AIRenderParams, + AIRenderResult, + Create2DMockupParams, + CreatePhotoMockupParams, + CreatePhotoMockupResult, + List2dMockupsParams, + ListPhotoMockupsParams, + PhotoMockup, + PhotoMockupAdjustments, + PhotoMockupDetails, + PhotoMockupListResult, + PhotoMockupPlacement, + PhotoMockupPrintArea, + PhotoMockupPrintAreaInput, + PhotoMockupPrintFile, + PhotoMockupQuad, + PhotoMockupRenderParams, + PhotoMockupRenderResult, + PhotoMockupSurface, + TwoDFullSurface, + TwoDMockup, + TwoDMockupDetails, + TwoDMockupListResult, + TwoDMockupQuad, + TwoDPrintAreaInput, + Update2DPrintAreasResult, + UpdatePhotoMockupPrintAreasResult, + WaitFor2DMockupOptions, + WaitForPhotoMockupOptions, +} from '../src/index' + +type Same = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 + ? true + : false + +export const familyNamesAreTheSameShapes: [ + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, + Same, +] = [ + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, +] diff --git a/tests/release.test.ts b/tests/release.test.ts index 3cba8bc..057ceed 100644 --- a/tests/release.test.ts +++ b/tests/release.test.ts @@ -66,6 +66,10 @@ const SHIPPED_ON_THE_WIRE = [ 'eventNaming', 'photo_mockup_create', 'photo_mockup.ready', + 'photoMockups', + 'psdMockups', + '/api/v1/photo-mockups', + '/api/v1/psd-mockups', ] as const describe('release consistency', () => { diff --git a/tests/renders.test.ts b/tests/renders.test.ts index a21faa5..c26854d 100644 --- a/tests/renders.test.ts +++ b/tests/renders.test.ts @@ -247,7 +247,7 @@ describe('renders.create()', () => { describe('ai.render() — 2D mockup', () => { it('renders artwork onto a 2D mockup and returns URL + renderUuid', async () => { const client = createClient() - const result = await client.ai.render({ + const result = await client.photoMockups.render({ mockupId: '11111111-1111-1111-1111-111111111111', printAreas: [ { @@ -269,12 +269,12 @@ describe('ai.render() — 2D mockup', () => { expect(result.printFiles[0]).not.toHaveProperty('privateStorageKey') }) - it('posts the 2D render body in snake_case to /sudoai/2d-mockups/{id}/render (id in path, not body)', async () => { + it('posts the 2D render body in snake_case to /photo-mockups/{id}/render (id in path, not body)', async () => { let capturedBody: Record = {} let capturedPath = '' server.use( http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, async ({ request }) => { capturedPath = new URL(request.url).pathname capturedBody = (await request.json()) as Record @@ -284,7 +284,7 @@ describe('ai.render() — 2D mockup', () => { ) const client = createClient() - await client.ai.render({ + await client.photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [ { @@ -296,7 +296,7 @@ describe('ai.render() — 2D mockup', () => { exportOptions: { imageFormat: 'webp', imageSize: 2048, quality: 90 }, }) - expect(capturedPath).toBe('/api/v1/sudoai/2d-mockups/mockup-uuid/render') + expect(capturedPath).toBe('/api/v1/photo-mockups/mockup-uuid/render') // Mockup id moved into the path -- it must NOT be in the body anymore. expect(capturedBody['mockup_uuid']).toBeUndefined() const printAreas = capturedBody['print_areas'] as Record[] @@ -318,7 +318,7 @@ describe('ai.render() — 2D mockup', () => { let capturedBody: Record = {} server.use( http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json(MOCK_AI_RENDER_RESPONSE) @@ -326,7 +326,7 @@ describe('ai.render() — 2D mockup', () => { ), ) - await createClient().ai.render({ + await createClient().photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [ { @@ -349,7 +349,7 @@ describe('ai.render() — 2D mockup', () => { let capturedBody: Record = {} server.use( http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json(MOCK_AI_RENDER_RESPONSE) @@ -357,7 +357,7 @@ describe('ai.render() — 2D mockup', () => { ), ) - await createClient().ai.render({ + await createClient().photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [ { @@ -381,7 +381,7 @@ describe('ai.render() — 2D mockup', () => { let capturedBody: Record = {} server.use( http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json(MOCK_AI_RENDER_RESPONSE) @@ -393,7 +393,7 @@ describe('ai.render() — 2D mockup', () => { // enforces it: handing a print area a coverage, or a surface a fit, does // not compile. This test used to send both on one target, which the API // now answers with a 422. - await createClient().ai.render({ + await createClient().photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [ { @@ -424,7 +424,7 @@ describe('ai.render() — 2D mockup', () => { let capturedBody: Record = {} server.use( http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json(MOCK_AI_RENDER_RESPONSE) @@ -432,7 +432,7 @@ describe('ai.render() — 2D mockup', () => { ), ) - await createClient().ai.render({ + await createClient().photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [{ surfaceUuid: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', @@ -453,14 +453,14 @@ describe('ai.render() — 2D mockup', () => { let capturedPath = '' server.use( http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, async ({ request }) => { capturedPath = new URL(request.url).pathname capturedBody = (await request.json()) as Record return HttpResponse.json( { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_render', + kind: 'photo_mockup_render', status: 'queued', status_url: `/api/v1/jobs/${TWO_D_CREATE_JOB_ID}`, }, @@ -470,7 +470,7 @@ describe('ai.render() — 2D mockup', () => { ), ) - const job = await createClient().ai.render({ + const job = await createClient().photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [ { uuid: 'pa-uuid', artworkUrl: 'https://example.com/design.png' }, @@ -479,13 +479,13 @@ describe('ai.render() — 2D mockup', () => { }) // Still the plural path-param URL; id stays in the path, not the body. - expect(capturedPath).toBe('/api/v1/sudoai/2d-mockups/mockup-uuid/render') + expect(capturedPath).toBe('/api/v1/photo-mockups/mockup-uuid/render') expect(capturedBody['is_async']).toBe(true) expect(capturedBody['mockup_uuid']).toBeUndefined() // 202 resolves with the job envelope (no printFiles read -> no crash). expect(job).toEqual({ jobId: TWO_D_CREATE_JOB_ID, - kind: '2d_render', + kind: 'photo_mockup_render', status: 'queued', statusUrl: `/api/v1/jobs/${TWO_D_CREATE_JOB_ID}`, }) @@ -495,12 +495,12 @@ describe('ai.render() — 2D mockup', () => { it('awaits an async 2D render via jobs.waitForJob to a result_url', async () => { server.use( http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, () => HttpResponse.json( { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_render', + kind: 'photo_mockup_render', status: 'queued', status_url: `/api/v1/jobs/${TWO_D_CREATE_JOB_ID}`, }, @@ -512,7 +512,7 @@ describe('ai.render() — 2D mockup', () => { success: true, data: { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_render', + kind: 'photo_mockup_render', status: 'succeeded', result_url: 'https://cdn.sudomock.com/renders/sudoai/async-done.png', error: null, @@ -522,7 +522,7 @@ describe('ai.render() — 2D mockup', () => { ) const client = createClient() - const job = await client.ai.render({ + const job = await client.photoMockups.render({ mockupId: 'mockup-uuid', printAreas: [ { uuid: 'pa-uuid', artworkUrl: 'https://example.com/design.png' }, @@ -543,7 +543,7 @@ describe('ai.create() and waitForReady()', () => { let capturedBody: Record = {} let idempotencyKey: string | null = null server.use( - http.post(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups`, async ({ request }) => { + http.post(`${TEST_BASE_URL}/api/v1/photo-mockups`, async ({ request }) => { capturedBody = (await request.json()) as Record idempotencyKey = request.headers.get('Idempotency-Key') return HttpResponse.json( @@ -553,7 +553,7 @@ describe('ai.create() and waitForReady()', () => { }), ) - const mockup = await createClient().ai.create({ + const mockup = await createClient().photoMockups.create({ sourceUrl: 'https://example.com/product.jpg', name: 'Front view', idempotencyKey: 'front-view-v1', @@ -582,7 +582,7 @@ describe('ai.create() and waitForReady()', () => { let capturedBody: Record = {} let idempotencyKey: string | null = null server.use( - http.post(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups`, async ({ request }) => { + http.post(`${TEST_BASE_URL}/api/v1/photo-mockups`, async ({ request }) => { capturedBody = (await request.json()) as Record idempotencyKey = request.headers.get('Idempotency-Key') return HttpResponse.json( @@ -592,7 +592,7 @@ describe('ai.create() and waitForReady()', () => { }), ) - await createClient().ai.create({ sourceBase64: 'aW1hZ2U=' }) + await createClient().photoMockups.create({ sourceBase64: 'aW1hZ2U=' }) expect(capturedBody).toEqual({ source_base64: 'aW1hZ2U=' }) expect(idempotencyKey).toMatch( @@ -603,12 +603,12 @@ describe('ai.create() and waitForReady()', () => { it('returns a Job (202) and sends is_async when isAsync: true', async () => { let capturedBody: Record = {} server.use( - http.post(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups`, async ({ request }) => { + http.post(`${TEST_BASE_URL}/api/v1/photo-mockups`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json( { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_create', + kind: 'photo_mockup_create', status: 'queued', status_url: `/api/v1/jobs/${TWO_D_CREATE_JOB_ID}`, }, @@ -617,7 +617,7 @@ describe('ai.create() and waitForReady()', () => { }), ) - const job = await createClient().ai.create({ + const job = await createClient().photoMockups.create({ sourceUrl: 'https://example.com/product.jpg', isAsync: true, }) @@ -628,7 +628,7 @@ describe('ai.create() and waitForReady()', () => { }) expect(job).toEqual({ jobId: TWO_D_CREATE_JOB_ID, - kind: '2d_create', + kind: 'photo_mockup_create', status: 'queued', statusUrl: `/api/v1/jobs/${TWO_D_CREATE_JOB_ID}`, }) @@ -637,7 +637,7 @@ describe('ai.create() and waitForReady()', () => { it('passes through seed print_areas (with names) on create', async () => { let capturedBody: Record = {} server.use( - http.post(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups`, async ({ request }) => { + http.post(`${TEST_BASE_URL}/api/v1/photo-mockups`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json( { success: true, data: MOCK_2D_MOCKUP }, @@ -646,7 +646,7 @@ describe('ai.create() and waitForReady()', () => { }), ) - await createClient().ai.create({ + await createClient().photoMockups.create({ sourceUrl: 'https://example.com/product.jpg', printAreas: [ { points: [[0, 0], [1, 0], [1, 1], [0, 1]], name: 'Front' }, @@ -660,9 +660,9 @@ describe('ai.create() and waitForReady()', () => { it('rejects both or neither source before making a request', async () => { const client = createClient() - await expect(client.ai.create({} as never)).rejects.toThrow(ValidationError) + await expect(client.photoMockups.create({} as never)).rejects.toThrow(ValidationError) await expect( - client.ai.create({ sourceUrl: 'https://example.com/x.jpg', sourceBase64: 'eA==' } as never), + client.photoMockups.create({ sourceUrl: 'https://example.com/x.jpg', sourceBase64: 'eA==' } as never), ).rejects.toThrow(ValidationError) }) @@ -673,21 +673,21 @@ describe('ai.create() and waitForReady()', () => { success: true, data: { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_create', + kind: 'photo_mockup_create', status: 'succeeded', mockup_uuid: MOCK_2D_MOCKUP.mockup_id, - result_url: `/api/v1/sudoai/2d-mockups/${MOCK_2D_MOCKUP.mockup_id}`, + result_url: `/api/v1/photo-mockups/${MOCK_2D_MOCKUP.mockup_id}`, }, }), ), - http.get(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id`, () => + http.get(`${TEST_BASE_URL}/api/v1/photo-mockups/:id`, () => HttpResponse.json({ success: true, data: MOCK_2D_MOCKUP }), ), ) - const mockup = await createClient().ai.waitForReady({ + const mockup = await createClient().photoMockups.waitForReady({ jobId: TWO_D_CREATE_JOB_ID, - kind: '2d_create', + kind: 'photo_mockup_create', status: 'queued', statusUrl: `/api/v1/jobs/${TWO_D_CREATE_JOB_ID}`, }) @@ -705,7 +705,7 @@ describe('ai.create() and waitForReady()', () => { success: true, data: { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_create', + kind: 'photo_mockup_create', status: 'succeeded', }, }), @@ -713,7 +713,7 @@ describe('ai.create() and waitForReady()', () => { ) await expect( - createClient().ai.waitForReady(TWO_D_CREATE_JOB_ID), + createClient().photoMockups.waitForReady(TWO_D_CREATE_JOB_ID), ).rejects.toMatchObject({ code: 'invalid_job_response' }) }) @@ -724,7 +724,7 @@ describe('ai.create() and waitForReady()', () => { success: true, data: { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_create', + kind: 'photo_mockup_create', status: 'failed', error: { error_code: 'NOT_MOCKUPABLE', @@ -735,7 +735,7 @@ describe('ai.create() and waitForReady()', () => { ), ) - const error = await createClient().ai.waitForReady(TWO_D_CREATE_JOB_ID, { + const error = await createClient().photoMockups.waitForReady(TWO_D_CREATE_JOB_ID, { intervalMs: 5, }).catch((caught: unknown) => caught) @@ -754,14 +754,14 @@ describe('ai.create() and waitForReady()', () => { success: true, data: { job_id: TWO_D_CREATE_JOB_ID, - kind: '2d_create', + kind: 'photo_mockup_create', status: 'running', }, }), ), ) - const error = await createClient().ai.waitForReady(TWO_D_CREATE_JOB_ID, { + const error = await createClient().photoMockups.waitForReady(TWO_D_CREATE_JOB_ID, { intervalMs: 5, timeoutMs: 20, }).catch((caught: unknown) => caught) @@ -775,7 +775,7 @@ describe('ai.create() and waitForReady()', () => { it('throws CreditError on 402', async () => { server.use( - http.post(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups`, () => + http.post(`${TEST_BASE_URL}/api/v1/photo-mockups`, () => HttpResponse.json( { detail: 'Insufficient credits' }, { status: 402 }, @@ -784,7 +784,7 @@ describe('ai.create() and waitForReady()', () => { ) await expect( - createClient().ai.create({ sourceUrl: 'https://example.com/product.jpg' }), + createClient().photoMockups.create({ sourceUrl: 'https://example.com/product.jpg' }), ).rejects.toThrow(CreditError) }) }) @@ -797,7 +797,7 @@ describe('ai.updatePrintAreas()', () => { }] server.use( http.put( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/print-areas`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/print-areas`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json({ @@ -815,7 +815,7 @@ describe('ai.updatePrintAreas()', () => { ), ) - const result = await createClient().ai.updatePrintAreas( + const result = await createClient().photoMockups.updatePrintAreas( MOCK_2D_MOCKUP.mockup_id, printAreas, ) @@ -834,7 +834,7 @@ describe('ai.updatePrintAreas()', () => { let capturedBody: Record = {} server.use( http.put( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/print-areas`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/print-areas`, async ({ request }) => { capturedBody = (await request.json()) as Record return HttpResponse.json({ @@ -848,10 +848,10 @@ describe('ai.updatePrintAreas()', () => { ), ) - await client.ai.updatePrintAreas(MOCK_2D_MOCKUP.mockup_id, []) + await client.photoMockups.updatePrintAreas(MOCK_2D_MOCKUP.mockup_id, []) expect(capturedBody).toEqual({ print_areas: [] }) await expect( - client.ai.updatePrintAreas( + client.photoMockups.updatePrintAreas( MOCK_2D_MOCKUP.mockup_id, Array(9).fill(printArea), ), @@ -863,7 +863,7 @@ describe('ai 2D-mockup catalog', () => { it('lists 2D mockups', async () => { let customizableOnly: string | null = null server.use( - http.get(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups`, ({ request }) => { + http.get(`${TEST_BASE_URL}/api/v1/photo-mockups`, ({ request }) => { customizableOnly = new URL(request.url).searchParams.get('customizable_only') return ( HttpResponse.json({ @@ -878,7 +878,7 @@ describe('ai 2D-mockup catalog', () => { ) const client = createClient() - const page = await client.ai.list({ limit: 20, customizableOnly: true }) + const page = await client.photoMockups.list({ limit: 20, customizableOnly: true }) // Pagination metadata (total/limit/offset) is surfaced alongside the page. expect(page.total).toBe(1) expect(page.limit).toBe(20) @@ -897,7 +897,7 @@ describe('ai 2D-mockup catalog', () => { it('gets a single 2D mockup', async () => { server.use( - http.get(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id`, () => + http.get(`${TEST_BASE_URL}/api/v1/photo-mockups/:id`, () => HttpResponse.json({ success: true, data: { @@ -915,7 +915,7 @@ describe('ai 2D-mockup catalog', () => { ) const client = createClient() - const mockup = await client.ai.get('99999999-9999-9999-9999-999999999999') + const mockup = await client.photoMockups.get('99999999-9999-9999-9999-999999999999') expect(mockup.name).toBe('2D Tee') expect(mockup.customizable).toBe(true) expect(mockup.quads?.[0]!.printAreaId).toBe( @@ -933,7 +933,7 @@ describe('ai 2D-mockup catalog', () => { it('deletes a 2D mockup', async () => { let hitPath = '' server.use( - http.delete(`${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id`, ({ request }) => { + http.delete(`${TEST_BASE_URL}/api/v1/photo-mockups/:id`, ({ request }) => { hitPath = new URL(request.url).pathname return HttpResponse.json({ success: true, data: { deleted: true } }) }), @@ -941,10 +941,10 @@ describe('ai 2D-mockup catalog', () => { const client = createClient() await expect( - client.ai.delete('99999999-9999-9999-9999-999999999999'), + client.photoMockups.delete('99999999-9999-9999-9999-999999999999'), ).resolves.toBeUndefined() expect(hitPath).toBe( - '/api/v1/sudoai/2d-mockups/99999999-9999-9999-9999-999999999999', + '/api/v1/photo-mockups/99999999-9999-9999-9999-999999999999', ) }) }) diff --git a/tests/resource-names.test.ts b/tests/resource-names.test.ts new file mode 100644 index 0000000..291052a --- /dev/null +++ b/tests/resource-names.test.ts @@ -0,0 +1,174 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { http, HttpResponse } from 'msw' +import SudoMock from '../src/index' +import { + MOCK_2D_MOCKUP, + MOCK_AI_RENDER_RESPONSE, + MOCK_MOCKUP, + TEST_API_KEY, + TEST_BASE_URL, + server, +} from './setup' + +function createClient() { + return new SudoMock(TEST_API_KEY, { baseUrl: TEST_BASE_URL }) +} + +const PHOTO_ID = MOCK_2D_MOCKUP.mockup_id +const PSD_ID = MOCK_MOCKUP.uuid + +// --------------------------------------------------------------------------- +// client.photoMockups / client.psdMockups are the accessors. client.ai and +// client.mockups return the same objects and warn once, so a caller on the +// earlier names keeps working and learns the new ones from the warning. +// --------------------------------------------------------------------------- + +describe('deprecated accessors', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('client.ai is client.photoMockups and warns once', () => { + const warn = vi.spyOn(process, 'emitWarning').mockImplementation(() => {}) + + const client = createClient() + expect(client.ai).toBe(client.photoMockups) + expect(client.ai).toBe(client.photoMockups) + expect(createClient().ai).toBeDefined() + + expect(warn).toHaveBeenCalledTimes(1) + const call = warn.mock.calls[0] as unknown[] | undefined + expect(String(call?.[0])).toContain('client.ai') + expect(String(call?.[0])).toContain('client.photoMockups') + expect(call?.[1]).toBe('DeprecationWarning') + }) + + it('client.mockups is client.psdMockups and warns once', () => { + const warn = vi.spyOn(process, 'emitWarning').mockImplementation(() => {}) + + const client = createClient() + expect(client.mockups).toBe(client.psdMockups) + expect(client.mockups).toBe(client.psdMockups) + expect(createClient().mockups).toBeDefined() + + expect(warn).toHaveBeenCalledTimes(1) + const call = warn.mock.calls[0] as unknown[] | undefined + expect(String(call?.[0])).toContain('client.mockups') + expect(String(call?.[0])).toContain('client.psdMockups') + expect(call?.[1]).toBe('DeprecationWarning') + }) +}) + +// --------------------------------------------------------------------------- +// Each family accessor talks to its family path, for every method it has. +// --------------------------------------------------------------------------- + +describe('family paths on the wire', () => { + it('client.photoMockups sends every call to /api/v1/photo-mockups', async () => { + const hits: string[] = [] + const record = (request: Request) => { + hits.push(`${request.method} ${new URL(request.url).pathname}`) + } + const base = `${TEST_BASE_URL}/api/v1/photo-mockups` + server.use( + http.post(base, ({ request }) => { + record(request) + return HttpResponse.json( + { success: true, data: MOCK_2D_MOCKUP }, + { status: 201 }, + ) + }), + http.get(base, ({ request }) => { + record(request) + return HttpResponse.json({ + success: true, + data: [MOCK_2D_MOCKUP], + total: 1, + limit: 20, + offset: 0, + }) + }), + http.get(`${base}/:id`, ({ request }) => { + record(request) + return HttpResponse.json({ success: true, data: MOCK_2D_MOCKUP }) + }), + http.put(`${base}/:id/print-areas`, ({ request }) => { + record(request) + return HttpResponse.json({ + success: true, + data: { mockup_id: PHOTO_ID, print_areas: [] }, + }) + }), + http.post(`${base}/:id/render`, ({ request }) => { + record(request) + return HttpResponse.json(MOCK_AI_RENDER_RESPONSE) + }), + http.delete(`${base}/:id`, ({ request }) => { + record(request) + return HttpResponse.json({ success: true, data: { deleted: true } }) + }), + ) + + const photoMockups = createClient().photoMockups + await photoMockups.create({ sourceUrl: 'https://example.com/product.jpg' }) + await photoMockups.list() + await photoMockups.get(PHOTO_ID) + await photoMockups.updatePrintAreas(PHOTO_ID, []) + await photoMockups.render({ + mockupId: PHOTO_ID, + printAreas: [{ uuid: 'pa-uuid', artworkUrl: 'https://example.com/design.png' }], + }) + await photoMockups.delete(PHOTO_ID) + + expect(hits).toEqual([ + 'POST /api/v1/photo-mockups', + 'GET /api/v1/photo-mockups', + `GET /api/v1/photo-mockups/${PHOTO_ID}`, + `PUT /api/v1/photo-mockups/${PHOTO_ID}/print-areas`, + `POST /api/v1/photo-mockups/${PHOTO_ID}/render`, + `DELETE /api/v1/photo-mockups/${PHOTO_ID}`, + ]) + }) + + it('client.psdMockups sends every call to /api/v1/psd-mockups', async () => { + const hits: string[] = [] + const record = (request: Request) => { + hits.push(`${request.method} ${new URL(request.url).pathname}`) + } + const base = `${TEST_BASE_URL}/api/v1/psd-mockups` + server.use( + http.get(base, ({ request }) => { + record(request) + return HttpResponse.json({ + success: true, + data: { mockups: [MOCK_MOCKUP], total: 1, limit: 20, offset: 0 }, + }) + }), + http.get(`${base}/:uuid`, ({ request }) => { + record(request) + return HttpResponse.json({ success: true, data: MOCK_MOCKUP }) + }), + http.patch(`${base}/:uuid`, ({ request }) => { + record(request) + return HttpResponse.json({ success: true, data: MOCK_MOCKUP }) + }), + http.delete(`${base}/:uuid`, ({ request }) => { + record(request) + return new HttpResponse(null, { status: 204 }) + }), + ) + + const psdMockups = createClient().psdMockups + await psdMockups.list() + await psdMockups.get(PSD_ID) + await psdMockups.update(PSD_ID, { name: 'Renamed' }) + await psdMockups.delete(PSD_ID) + + expect(hits).toEqual([ + 'GET /api/v1/psd-mockups', + `GET /api/v1/psd-mockups/${PSD_ID}`, + `PATCH /api/v1/psd-mockups/${PSD_ID}`, + `DELETE /api/v1/psd-mockups/${PSD_ID}`, + ]) + }) +}) diff --git a/tests/setup.ts b/tests/setup.ts index 6f0591c..b677742 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -271,7 +271,7 @@ export const MOCK_WEBHOOK_DELIVERY = { export const handlers = [ // List mockups - http.get(`${TEST_BASE_URL}/api/v1/mockups`, ({ request }) => { + http.get(`${TEST_BASE_URL}/api/v1/psd-mockups`, ({ request }) => { const apiKey = request.headers.get('x-api-key') if (apiKey !== TEST_API_KEY) { return HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }) @@ -288,7 +288,7 @@ export const handlers = [ }), // Get mockup - http.get(`${TEST_BASE_URL}/api/v1/mockups/:uuid`, ({ request, params }) => { + http.get(`${TEST_BASE_URL}/api/v1/psd-mockups/:uuid`, ({ request, params }) => { const apiKey = request.headers.get('x-api-key') if (apiKey !== TEST_API_KEY) { return HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }) @@ -303,7 +303,7 @@ export const handlers = [ }), // Update mockup - http.patch(`${TEST_BASE_URL}/api/v1/mockups/:uuid`, ({ request }) => { + http.patch(`${TEST_BASE_URL}/api/v1/psd-mockups/:uuid`, ({ request }) => { const apiKey = request.headers.get('x-api-key') if (apiKey !== TEST_API_KEY) { return HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }) @@ -315,7 +315,7 @@ export const handlers = [ }), // Delete mockup - http.delete(`${TEST_BASE_URL}/api/v1/mockups/:uuid`, ({ request }) => { + http.delete(`${TEST_BASE_URL}/api/v1/psd-mockups/:uuid`, ({ request }) => { const apiKey = request.headers.get('x-api-key') if (apiKey !== TEST_API_KEY) { return HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }) @@ -334,7 +334,7 @@ export const handlers = [ // AI 2D-mockup Render (mockup id in path) http.post( - `${TEST_BASE_URL}/api/v1/sudoai/2d-mockups/:id/render`, + `${TEST_BASE_URL}/api/v1/photo-mockups/:id/render`, ({ request }) => { const apiKey = request.headers.get('x-api-key') if (apiKey !== TEST_API_KEY) {