Monorepo of test data builders for commercetools platform entities. Teams across the organization use these builders to generate realistic mock data for both REST and GraphQL API responses in their test suites.
Two workspace packages (pnpm-workspace.yaml):
standalone/— the published package (@commercetools/composable-commerce-test-data). Contains all domain models undersrc/models/, organized by domain area (product, cart, category, etc.). Each domain folder (e.g.product/) contains one or more sub-model folders (e.g.product/,product-draft/), and each sub-model folder containstypes.ts,fields-config.ts,builders.ts,builders.spec.ts,index.ts, and an optionalpresets/directory. Thesrc/core/module exportscreateSpecializedBuilder,fake,sequence,oneOf,buildLimitGraphqlList,buildCountGraphqlList, and other helpers. All domain models depend on core — changes here have blast-radius across every model. Dependencies beyond root stack:@commercetools/platform-sdk(REST types),@faker-js/faker(random data generation),lodash,omit-deep.generators/— internal CLI tool (pnpm generate-model) for scaffolding new test data models. Aprompts-based CLI entry point (src/index.ts) dispatches to generators — currently the only generator isnew-test-model. Templates live insrc/new-test-model/templates/and use Squirrelly for rendering. This package usestsxto run TypeScript directly — it is not built bypreconstruct.
Every model has two representations — REST and GraphQL — each with a random
builder and optional presets. REST types come from @commercetools/platform-sdk.
GraphQL types are generated via graphql-codegen from introspection schemas
stored in schemas/ (core, ctp, mc, settings). A types-post-processor.mjs
runs after codegen to un-export colliding helper types and replace any with
unknown.
preconstruct builds the standalone package — each domain model is a separate
entrypoint (see standalone/package.json preconstruct.entrypoints).
All imports within standalone/ use path aliases (defined in tsconfig.json,
mirrored in babel.config.js):
| Alias | Resolves to |
|---|---|
@/core |
standalone/src/core |
@/core/test-utils |
standalone/src/core/test-utils |
@/graphql-types |
standalone/src/graphql-types |
@/models/* |
standalone/src/models/* |
@/utils |
standalone/src/utils |
Do not use relative imports to reach across these boundaries.
createSpecializedBuilder({ name, type, modelFieldsConfig })— creates a builder for a single API type ('rest'or'graphql'). Use forRestModelBuilderandGraphqlModelBuilder.createCompatibilityBuilder({ name, modelFieldsConfig: { rest, graphql } })— creates a builder that supports all three build methods. Deprecated for new models; prefer specialized builders.
Every builder is a Proxy with:
.build()— returns the built object (REST for specialized rest builders, REST for compat builders)..buildRest()/.buildGraphql()— explicitly build one representation..fieldName(value)— fluent setter for any model field. Returns the builder for chaining. Value can be a literal, a nested builder (auto-built), or a function(currentState) => Partial<Model>..build({ omitFields: ['a'] })/.build({ keepFields: ['a'] })— include or exclude specific fields from the output.
Each model exports restFieldsConfig and graphqlFieldsConfig of type
TModelFieldsConfig<T>:
export const restFieldsConfig: TModelFieldsConfig<TMyModelRest> = {
fields: {
id: fake((f) => f.string.uuid()), // callback receives a Faker instance
version: sequence(), // auto-incrementing number per build
status: oneOf('Active', 'Inactive'), // random pick
active: bool(), // random true/false
name: fake(() => LocalizedString.random()), // nested builder (auto-built)
},
};Only assign values to required fields. Use presets for fully-populated versions.
GraphQL configs for non-draft models must include __typename as a string
literal (e.g. __typename: 'Category'). Draft models must not include it
(see ADR 0002).
Use postBuild when a field's value depends on other generated fields — most
commonly in GraphQL configs where singular fields are derived from
*AllLocales arrays:
export const graphqlFieldsConfig: TModelFieldsConfig<TMyModelGraphql> = {
fields: {
name: null,
nameAllLocales: fake(() => LocalizedString.random()) /* ... */,
},
postBuild: (model) => ({
...model,
name: LocalizedString.resolveGraphqlDefaultLocaleValue(
model.nameAllLocales
),
}),
};In compat builders, postBuild receives a second arg { isCompatMode: boolean }
to handle shape differences between REST and GraphQL field names.
Presets live in presets/ next to the model and return builders (not built
objects) so consumers can chain further overrides before calling .build().
Each preset file exports up to three variants (restPreset, graphqlPreset,
compatPreset). The presets/index.ts collects them:
export const restPresets = { withAllFields: restPreset };
export const graphqlPresets = { withAllFields: graphqlPreset };
export const compatPresets = { withAllFields: compatPreset };Each sub-model's index.ts wires builders and presets into the public API:
export const MyModelRest = {
presets: presets.restPresets,
random: RestModelBuilder,
};
export const MyModelGraphql = {
presets: presets.graphqlPresets,
random: GraphqlModelBuilder,
};Consumers use: MyModelRest.random().fieldName(value).build().
| Task | Command | Notes |
|---|---|---|
| Run tests | pnpm test |
Jest, matches **/*.spec.{js,ts} |
| Run a single test | pnpm test -- --testPathPattern=<path> |
Path fragment is enough |
| Typecheck | pnpm typecheck |
tsc --noEmit from root |
| Lint | pnpm lint |
Jest runner with ESLint |
Create a new model:
- Run
pnpm generate-model— the CLI scaffoldstypes.ts,fields-config.ts,builders.ts,builders.spec.ts, andindex.tswith TODOs. - Define REST and GraphQL types in
types.ts(REST from@commercetools/platform-sdk, GraphQL from@commercetools-test-data/graphql-types). - Implement
restFieldsConfigandgraphqlFieldsConfiginfields-config.ts. Only assign values to required properties — use presets for fully-populated versions. - Wire up builders in
builders.tsusingcreateSpecializedBuilder. - Re-export from the domain's
index.tsand add the entrypoint tostandalone/package.jsonpreconstruct.entrypoints. Add re-exports from the top-level barrel files (e.g.src/product.tsre-exports fromsrc/models/product/). Update thefilesarray instandalone/package.json. - Write builder specs validating default REST and GraphQL output shapes.
- Run
pnpm test,pnpm typecheck, andpnpm lint.
Update GraphQL types after schema changes:
- Copy
.env.templateto.envand fill in credentials (if not already done). - Run
pnpm generate-types— this regenerates types instandalone/src/graphql-types/generated/. Do not edit generated files manually.
Add a changeset before opening a PR:
- Run
pnpm changesetand follow the prompts to select affected packages and semver bump type.
- Published:
@commercetools/composable-commerce-test-data(standalone) — public npm, semver obligations apply. Changesets are required for publishable changes. - Internal-only:
@commercetools-test-data/generators— private, never published. - REST types come from
@commercetools/platform-sdk— do not manually define them. - GraphQL types are generated by
graphql-codegen— rungenerate-typesto update, do not edit files instandalone/src/graphql-types/generated/manually. - Preset ownership: presets under team-specific folders (e.g.
change-history-data,sample-data-fashion,sample-data-b2c-lifestyle) are owned by their respective teams and must not be altered without that team's review.
preconstruct devruns duringpostinstalland creates symlinks instandalone/for local dev. If you see missing module errors, runpnpm installfirst.types-post-processor.mjsruns after codegen to un-export helper types and replaceanywithunknown. Do not manually edit generated type files — your changes will be overwritten.- Draft GraphQL models must NOT include
__typename(see ADR 0002). This is a deliberate design decision for mutation input compatibility. - The pre-commit hook runs
lint-staged(prettier + eslint + tsc-files on changed files). Thecommit-msghook enforces conventional commits viacommitlint. @faker-js/fakeris ESM-only since v10 — Jest is configured with a customtransformIgnorePatternsto handle this. Do not add faker to the ignore list.- The
prettierPathinjest.test.config.jspoints toprettier-jest(an older version) because the latest Prettier is incompatible with Jest's snapshot formatting. src/core/is high blast-radius — it is the foundation for every model builder. Changes here affect all models. Run the full test suite after any core change.- Entrypoint registration is manual — if you add a new model directory under
src/models/but forget to add it topreconstruct.entrypointsinstandalone/package.json, it won't be included in the published build. - The
filesarray instandalone/package.jsonalso needs updating when adding a new domain — it controls what gets published to npm.
- Commits: conventional commit format (enforced by commitlint). Scopes with
slashes are allowed, e.g.
refactor(app/my-component): something. - Model file structure: each domain folder contains one or more sub-model
folders (e.g.
product/product/,product/product-draft/). Every sub-model must havetypes.ts,fields-config.ts,builders.ts,builders.spec.ts, andindex.ts. Presets go in apresets/subdirectory. - Fields config: only assign values to required properties. For fully-populated
versions, create a
withAllFieldspreset. - Changesets: required for publishable changes. Run
pnpm changeset— seedocs/guidelines/writing-changesets.mdfor content guidelines.
docs/contributing/test-data-models-overview.md— model architecture and public APIdocs/guidelines/creating-new-model.md— step-by-step model creation guidedocs/guidelines/writing-changesets.md— changeset content guidelinesdocs/architecture-decisions/— ADRs (notably ADR 0002: draft model__typenameexclusion)