@entifix/testing-e2e 0.8.0 → 0.9.0
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.
- package/dist/fixtures/entity-backend.d.ts +2 -0
- package/dist/fixtures/entity-backend.d.ts.map +1 -1
- package/dist/fixtures/entity-backend.js +35 -3
- package/dist/fixtures/entity-backend.js.map +1 -1
- package/dist/fixtures/entity-handlers.d.ts.map +1 -1
- package/dist/fixtures/entity-handlers.js +2 -0
- package/dist/fixtures/entity-handlers.js.map +1 -1
- package/package.json +10 -10
|
@@ -20,6 +20,8 @@ export interface EntityBackend {
|
|
|
20
20
|
readonly key: string;
|
|
21
21
|
/** `GET /api/<key>?<search>` — filtering, sorting and paging included. */
|
|
22
22
|
list(search: URLSearchParams): Promise<BackendResponse>;
|
|
23
|
+
/** `GET /api/<key>/$facets?<search>` — the counts beside a listing's filters. */
|
|
24
|
+
facets(search: URLSearchParams): Promise<BackendResponse>;
|
|
23
25
|
/** `GET /api/<key>/:id`. */
|
|
24
26
|
get(id: EntityId): Promise<BackendResponse>;
|
|
25
27
|
/** Replaces the stored rows, so one fixture can serve several journeys. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-backend.d.ts","sourceRoot":"","sources":["../../src/fixtures/entity-backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entity-backend.d.ts","sourceRoot":"","sources":["../../src/fixtures/entity-backend.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EAQd,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,KAAK,WAAW,EAEjB,MAAM,+BAA+B,CAAC;AAGvC;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,8EAA8E;AAC9E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,IAAI,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACxD,iFAAiF;IACjF,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC1D,4BAA4B;IAC5B,GAAG,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,2EAA2E;IAC3E,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IAC5C,wEAAwE;IACxE,IAAI,IAAI,UAAU,EAAE,CAAC;IACrB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;CAC1B;AAcD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,SAAS,MAAM,EACtD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,WAAe,oBAAyB,KACvC,aAkIF,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
-
import { EntityIdTag, EntityLoadRequestTag, EntityRepositoryTag, getUCFactory, loadUCFactory } from '@entifix/business';
|
|
3
|
-
import { envelopeEntityName, makeEntityEnvelope, makeEntityPageEnvelope, parseLoadRequestParams } from '@entifix/core';
|
|
4
|
-
import { makeMongoRepository } from '@entifix/mongo';
|
|
2
|
+
import { EntityFacetCounterTag, EntityIdTag, EntityLoadRequestTag, EntityRepositoryTag, facetCountsUCFactory, getUCFactory, loadUCFactory } from '@entifix/business';
|
|
3
|
+
import { envelopeEntityName, makeEntityEnvelope, makeEntityFacetsEnvelope, makeEntityPageEnvelope, parseFacetQueryParams, parseLoadRequestParams } from '@entifix/core';
|
|
4
|
+
import { makeMongoFacetCounter, makeMongoRepository } from '@entifix/mongo';
|
|
5
5
|
import { makeFakeMongoDb } from '@entifix/testing-unit/drivers';
|
|
6
6
|
import { Effect } from 'effect';
|
|
7
7
|
const collectionLinks = (key)=>[
|
|
@@ -66,6 +66,7 @@ const entityLinks = (key, id)=>[
|
|
|
66
66
|
const repository = makeMongoRepository(// The driver fake is typed `unknown` so `testing-unit` need not depend on
|
|
67
67
|
// the mongodb types; the cast is the documented way to hand it over.
|
|
68
68
|
db.db, entityConstructor);
|
|
69
|
+
const counter = makeMongoFacetCounter(db.db, entityConstructor);
|
|
69
70
|
const run = (effect)=>Effect.runPromise(effect);
|
|
70
71
|
return {
|
|
71
72
|
key,
|
|
@@ -105,6 +106,37 @@ const entityLinks = (key, id)=>[
|
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
},
|
|
109
|
+
// The same production pipeline as `list`: the protocol's parser and its
|
|
110
|
+
// allowlist, the use case, and the Mongo counter over the fake driver.
|
|
111
|
+
facets: async (search)=>{
|
|
112
|
+
let queries;
|
|
113
|
+
try {
|
|
114
|
+
queries = parseFacetQueryParams(entityConstructor, search);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
status: 400,
|
|
118
|
+
body: {
|
|
119
|
+
error: 'invalid query',
|
|
120
|
+
detail: String(error)
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const counts = await run(facetCountsUCFactory(queries).pipe(Effect.provideService(EntityFacetCounterTag, counter)));
|
|
126
|
+
return {
|
|
127
|
+
status: 200,
|
|
128
|
+
body: makeEntityFacetsEnvelope(entityConstructor, counts)
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
status: 500,
|
|
133
|
+
body: {
|
|
134
|
+
error: 'request failed',
|
|
135
|
+
detail: String(error)
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
},
|
|
108
140
|
get: async (id)=>{
|
|
109
141
|
const entity = await run(getUCFactory().pipe(Effect.provideService(EntityRepositoryTag, repository), Effect.provideService(EntityIdTag, id), Effect.option));
|
|
110
142
|
return entity._tag === 'None' ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fixtures/entity-backend.ts"],"sourcesContent":["import {\n EntityIdTag,\n EntityLoadRequestTag,\n EntityRepositoryTag,\n getUCFactory,\n loadUCFactory,\n} from '@entifix/business';\nimport {\n type EntifixEnvelopeLink,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n envelopeEntityName,\n makeEntityEnvelope,\n makeEntityPageEnvelope,\n parseLoadRequestParams,\n} from '@entifix/core';\nimport { makeMongoRepository } from '@entifix/mongo';\nimport {\n type FakeMongoDb,\n makeFakeMongoDb,\n} from '@entifix/testing-unit/drivers';\nimport { Effect } from 'effect';\n\n/**\n * A response as an HTTP transport would send it. `body` is an entifix envelope\n * or an error record; it stays `unknown` here so the backend is not coupled to\n * one transport's body type.\n */\nexport interface BackendResponse {\n readonly status: number;\n readonly body: unknown;\n}\n\n/** Rows as they are stored — the entity wire shape, exactly like the seed. */\nexport type BackendRow = Record<string, unknown>;\n\nexport interface EntityBackendOptions {\n /** Initial documents, in the entity wire shape. */\n seed?: ReadonlyArray<BackendRow>;\n}\n\nexport interface EntityBackend {\n /** The entity `key` — collection name and REST path segment alike. */\n readonly key: string;\n /** `GET /api/<key>?<search>` — filtering, sorting and paging included. */\n list(search: URLSearchParams): Promise<BackendResponse>;\n /** `GET /api/<key>/:id`. */\n get(id: EntityId): Promise<BackendResponse>;\n /** Replaces the stored rows, so one fixture can serve several journeys. */\n seed(rows: ReadonlyArray<BackendRow>): void;\n /** The rows currently stored, for assertions about what a write did. */\n rows(): BackendRow[];\n /** The underlying driver fake, for failure injection. */\n readonly db: FakeMongoDb;\n}\n\nconst collectionLinks = (key: string): EntifixEnvelopeLink[] => [\n { rel: 'self', href: `/api/${key}`, method: 'GET' },\n { rel: 'create', href: `/api/${key}`, method: 'POST' },\n];\n\nconst entityLinks = (key: string, id: EntityId): EntifixEnvelopeLink[] => [\n { rel: 'self', href: `/api/${key}/${String(id)}`, method: 'GET' },\n { rel: 'list', href: `/api/${key}`, method: 'GET' },\n { rel: 'update', href: `/api/${key}/${String(id)}`, method: 'PUT' },\n { rel: 'delete', href: `/api/${key}/${String(id)}`, method: 'DELETE' },\n];\n\n/**\n * A read backend for one entity, assembled from the PRODUCTION query pipeline:\n *\n * ```\n * parseLoadRequestParams → loadUCFactory → makeMongoRepository → fake driver\n * (core: RSQL + the (business: (mongo-client: (testing-unit)\n * filterable/sortable the use-case) filter translation)\n * allowlist)\n * ```\n *\n * Nothing here re-implements query semantics. That is the whole point: a mock\n * backend that answers a filter with \"all the rows\" makes a green `mock` run\n * meaningless, and a mock backend that implements its *own* RSQL makes the two\n * profiles disagree the first time an operator is added. Because the real\n * pipeline runs, `mock` and `live` agree on filtering, sorting, paging, and on\n * the `400 invalid query` the metadata allowlist produces.\n *\n * Writes are deliberately absent: a `POST` in the admin service is a\n * *transaction* answering `202`, and a fixture that answered `201` instead\n * would be a lie about the protocol rather than a stand-in for it.\n */\nexport const makeEntityBackend = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n { seed = [] }: EntityBackendOptions = {},\n): EntityBackend => {\n const key = envelopeEntityName(entityConstructor);\n const db = makeFakeMongoDb({ [key]: seed.map(row => ({ ...row })) });\n const repository = makeMongoRepository(\n // The driver fake is typed `unknown` so `testing-unit` need not depend on\n // the mongodb types; the cast is the documented way to hand it over.\n db.db as Parameters<typeof makeMongoRepository>[0],\n entityConstructor,\n );\n\n const run = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, never>,\n ): Promise<TValue> => Effect.runPromise(effect);\n\n return {\n key,\n\n list: async search => {\n let request: EntityLoadRequest;\n try {\n request = parseLoadRequestParams(\n entityConstructor,\n search,\n ) as unknown as EntityLoadRequest;\n } catch (error) {\n // The same 400 the service answers: an unparseable expression, or a\n // member the entity never declared filterable/sortable. The codec only\n // ever throws `EntifixBuildError` here, so there is no second case to\n // discriminate — anything else would be a bug in the codec, and\n // reporting it verbatim is more useful than hiding it behind a 500.\n return {\n status: 400,\n body: { error: 'invalid query', detail: String(error) },\n };\n }\n\n try {\n const page = await run(\n loadUCFactory<TEntity>().pipe(\n Effect.provideService(EntityRepositoryTag, repository),\n Effect.provideService(EntityLoadRequestTag, request),\n ),\n );\n\n return {\n status: 200,\n body: makeEntityPageEnvelope(\n entityConstructor,\n page,\n collectionLinks(key),\n ),\n };\n } catch (error) {\n // A repository that fails is a 500, exactly as in the service: the\n // request was valid, the store was not reachable.\n return {\n status: 500,\n body: { error: 'request failed', detail: String(error) },\n };\n }\n },\n\n get: async id => {\n const entity = await run(\n getUCFactory<TEntity>().pipe(\n Effect.provideService(EntityRepositoryTag, repository),\n Effect.provideService(EntityIdTag, id),\n Effect.option,\n ),\n );\n\n return entity._tag === 'None'\n ? { status: 404, body: { message: `${key} not found` } }\n : {\n status: 200,\n body: makeEntityEnvelope(\n entityConstructor,\n entity.value,\n entityLinks(key, id),\n ),\n };\n },\n\n seed: rows => {\n db.seed(\n key,\n rows.map(row => ({ ...row })),\n );\n },\n\n rows: () => db.read(key),\n\n db,\n };\n};\n"],"names":["EntityIdTag","EntityLoadRequestTag","EntityRepositoryTag","getUCFactory","loadUCFactory","envelopeEntityName","makeEntityEnvelope","makeEntityPageEnvelope","parseLoadRequestParams","makeMongoRepository","makeFakeMongoDb","Effect","collectionLinks","key","rel","href","method","entityLinks","id","String","makeEntityBackend","entityConstructor","seed","db","map","row","repository","run","effect","runPromise","list","search","request","error","status","body","detail","page","pipe","provideService","get","entity","option","_tag","message","value","rows","read"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/fixtures/entity-backend.ts"],"sourcesContent":["import {\n EntityFacetCounterTag,\n EntityIdTag,\n EntityLoadRequestTag,\n EntityRepositoryTag,\n facetCountsUCFactory,\n getUCFactory,\n loadUCFactory,\n} from '@entifix/business';\nimport {\n type EntifixEnvelopeLink,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n envelopeEntityName,\n makeEntityEnvelope,\n makeEntityFacetsEnvelope,\n makeEntityPageEnvelope,\n parseFacetQueryParams,\n parseLoadRequestParams,\n} from '@entifix/core';\nimport { makeMongoFacetCounter, makeMongoRepository } from '@entifix/mongo';\nimport {\n type FakeMongoDb,\n makeFakeMongoDb,\n} from '@entifix/testing-unit/drivers';\nimport { Effect } from 'effect';\n\n/**\n * A response as an HTTP transport would send it. `body` is an entifix envelope\n * or an error record; it stays `unknown` here so the backend is not coupled to\n * one transport's body type.\n */\nexport interface BackendResponse {\n readonly status: number;\n readonly body: unknown;\n}\n\n/** Rows as they are stored — the entity wire shape, exactly like the seed. */\nexport type BackendRow = Record<string, unknown>;\n\nexport interface EntityBackendOptions {\n /** Initial documents, in the entity wire shape. */\n seed?: ReadonlyArray<BackendRow>;\n}\n\nexport interface EntityBackend {\n /** The entity `key` — collection name and REST path segment alike. */\n readonly key: string;\n /** `GET /api/<key>?<search>` — filtering, sorting and paging included. */\n list(search: URLSearchParams): Promise<BackendResponse>;\n /** `GET /api/<key>/$facets?<search>` — the counts beside a listing's filters. */\n facets(search: URLSearchParams): Promise<BackendResponse>;\n /** `GET /api/<key>/:id`. */\n get(id: EntityId): Promise<BackendResponse>;\n /** Replaces the stored rows, so one fixture can serve several journeys. */\n seed(rows: ReadonlyArray<BackendRow>): void;\n /** The rows currently stored, for assertions about what a write did. */\n rows(): BackendRow[];\n /** The underlying driver fake, for failure injection. */\n readonly db: FakeMongoDb;\n}\n\nconst collectionLinks = (key: string): EntifixEnvelopeLink[] => [\n { rel: 'self', href: `/api/${key}`, method: 'GET' },\n { rel: 'create', href: `/api/${key}`, method: 'POST' },\n];\n\nconst entityLinks = (key: string, id: EntityId): EntifixEnvelopeLink[] => [\n { rel: 'self', href: `/api/${key}/${String(id)}`, method: 'GET' },\n { rel: 'list', href: `/api/${key}`, method: 'GET' },\n { rel: 'update', href: `/api/${key}/${String(id)}`, method: 'PUT' },\n { rel: 'delete', href: `/api/${key}/${String(id)}`, method: 'DELETE' },\n];\n\n/**\n * A read backend for one entity, assembled from the PRODUCTION query pipeline:\n *\n * ```\n * parseLoadRequestParams → loadUCFactory → makeMongoRepository → fake driver\n * (core: RSQL + the (business: (mongo-client: (testing-unit)\n * filterable/sortable the use-case) filter translation)\n * allowlist)\n * ```\n *\n * Nothing here re-implements query semantics. That is the whole point: a mock\n * backend that answers a filter with \"all the rows\" makes a green `mock` run\n * meaningless, and a mock backend that implements its *own* RSQL makes the two\n * profiles disagree the first time an operator is added. Because the real\n * pipeline runs, `mock` and `live` agree on filtering, sorting, paging, and on\n * the `400 invalid query` the metadata allowlist produces.\n *\n * Writes are deliberately absent: a `POST` in the admin service is a\n * *transaction* answering `202`, and a fixture that answered `201` instead\n * would be a lie about the protocol rather than a stand-in for it.\n */\nexport const makeEntityBackend = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n { seed = [] }: EntityBackendOptions = {},\n): EntityBackend => {\n const key = envelopeEntityName(entityConstructor);\n const db = makeFakeMongoDb({ [key]: seed.map(row => ({ ...row })) });\n const repository = makeMongoRepository(\n // The driver fake is typed `unknown` so `testing-unit` need not depend on\n // the mongodb types; the cast is the documented way to hand it over.\n db.db as Parameters<typeof makeMongoRepository>[0],\n entityConstructor,\n );\n\n const counter = makeMongoFacetCounter(\n db.db as Parameters<typeof makeMongoFacetCounter>[0],\n entityConstructor,\n );\n\n const run = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, never>,\n ): Promise<TValue> => Effect.runPromise(effect);\n\n return {\n key,\n\n list: async search => {\n let request: EntityLoadRequest;\n try {\n request = parseLoadRequestParams(\n entityConstructor,\n search,\n ) as unknown as EntityLoadRequest;\n } catch (error) {\n // The same 400 the service answers: an unparseable expression, or a\n // member the entity never declared filterable/sortable. The codec only\n // ever throws `EntifixBuildError` here, so there is no second case to\n // discriminate — anything else would be a bug in the codec, and\n // reporting it verbatim is more useful than hiding it behind a 500.\n return {\n status: 400,\n body: { error: 'invalid query', detail: String(error) },\n };\n }\n\n try {\n const page = await run(\n loadUCFactory<TEntity>().pipe(\n Effect.provideService(EntityRepositoryTag, repository),\n Effect.provideService(EntityLoadRequestTag, request),\n ),\n );\n\n return {\n status: 200,\n body: makeEntityPageEnvelope(\n entityConstructor,\n page,\n collectionLinks(key),\n ),\n };\n } catch (error) {\n // A repository that fails is a 500, exactly as in the service: the\n // request was valid, the store was not reachable.\n return {\n status: 500,\n body: { error: 'request failed', detail: String(error) },\n };\n }\n },\n\n // The same production pipeline as `list`: the protocol's parser and its\n // allowlist, the use case, and the Mongo counter over the fake driver.\n facets: async search => {\n let queries;\n try {\n queries = parseFacetQueryParams(entityConstructor, search);\n } catch (error) {\n return {\n status: 400,\n body: { error: 'invalid query', detail: String(error) },\n };\n }\n\n try {\n const counts = await run(\n facetCountsUCFactory(queries).pipe(\n Effect.provideService(EntityFacetCounterTag, counter),\n ),\n );\n return {\n status: 200,\n body: makeEntityFacetsEnvelope(entityConstructor, counts),\n };\n } catch (error) {\n return {\n status: 500,\n body: { error: 'request failed', detail: String(error) },\n };\n }\n },\n\n get: async id => {\n const entity = await run(\n getUCFactory<TEntity>().pipe(\n Effect.provideService(EntityRepositoryTag, repository),\n Effect.provideService(EntityIdTag, id),\n Effect.option,\n ),\n );\n\n return entity._tag === 'None'\n ? { status: 404, body: { message: `${key} not found` } }\n : {\n status: 200,\n body: makeEntityEnvelope(\n entityConstructor,\n entity.value,\n entityLinks(key, id),\n ),\n };\n },\n\n seed: rows => {\n db.seed(\n key,\n rows.map(row => ({ ...row })),\n );\n },\n\n rows: () => db.read(key),\n\n db,\n };\n};\n"],"names":["EntityFacetCounterTag","EntityIdTag","EntityLoadRequestTag","EntityRepositoryTag","facetCountsUCFactory","getUCFactory","loadUCFactory","envelopeEntityName","makeEntityEnvelope","makeEntityFacetsEnvelope","makeEntityPageEnvelope","parseFacetQueryParams","parseLoadRequestParams","makeMongoFacetCounter","makeMongoRepository","makeFakeMongoDb","Effect","collectionLinks","key","rel","href","method","entityLinks","id","String","makeEntityBackend","entityConstructor","seed","db","map","row","repository","counter","run","effect","runPromise","list","search","request","error","status","body","detail","page","pipe","provideService","facets","queries","counts","get","entity","option","_tag","message","value","rows","read"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-handlers.d.ts","sourceRoot":"","sources":["../../src/fixtures/entity-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE9D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,2BAA4B,SAAQ,oBAAoB;IACvE,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,SAAS,MAAM,EAC1D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,yBAAyB,2BAA2B,KACnD;IAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"entity-handlers.d.ts","sourceRoot":"","sources":["../../src/fixtures/entity-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE9D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,2BAA4B,SAAQ,oBAAoB;IACvE,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,SAAS,MAAM,EAC1D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,yBAAyB,2BAA2B,KACnD;IAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAuBtD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAC/B,KAAK,MAAM,EACX,eAAe,kBAAkB,KAChC,cAAuE,CAAC;AAE3E,YAAY,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC"}
|
|
@@ -22,6 +22,8 @@ import { makeEntityBackend } from './entity-backend.js';
|
|
|
22
22
|
backend,
|
|
23
23
|
handlers: [
|
|
24
24
|
http.get(baseUrl, async ({ request })=>respond(await backend.list(new URL(request.url).searchParams))),
|
|
25
|
+
// Before `/:id`, which would otherwise take `$facets` for an id.
|
|
26
|
+
http.get(`${baseUrl}/$facets`, async ({ request })=>respond(await backend.facets(new URL(request.url).searchParams))),
|
|
25
27
|
http.get(`${baseUrl}/:id`, async ({ params })=>respond(await backend.get(String(params['id']))))
|
|
26
28
|
]
|
|
27
29
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fixtures/entity-handlers.ts"],"sourcesContent":["import type { ConfigurationPlain } from '@entifix/core';\nimport type { Entity, EntityConstructor } from '@entifix/core';\nimport { http, HttpResponse, type RequestHandler } from 'msw';\n\nimport {\n type BackendResponse,\n type EntityBackend,\n type EntityBackendOptions,\n makeEntityBackend,\n} from './entity-backend.js';\n\nexport interface EntityBackendHandlerOptions extends EntityBackendOptions {\n /** The collection endpoint, e.g. `http://localhost:3101/api/product-brand`. */\n baseUrl: string;\n}\n\n/**\n * Puts an {@link makeEntityBackend} behind msw handlers, so a browser under\n * Playwright talks to the real query pipeline over the real wire format.\n *\n * The backend is returned alongside the handlers because a spec usually wants\n * both: the handlers to install, and the backend to reseed or to break with\n * `backend.db.failWith(...)`.\n */\nexport const entityBackendHandlers = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n { baseUrl, ...options }: EntityBackendHandlerOptions,\n): { handlers: RequestHandler[]; backend: EntityBackend } => {\n const backend = makeEntityBackend(entityConstructor, options);\n\n // Every body is an envelope or an error record — JSON either way. The cast is\n // the one place the backend's transport-free `unknown` meets msw's body type.\n const respond = ({ status, body }: BackendResponse) =>\n HttpResponse.json(body as Record<string, unknown>, { status });\n\n return {\n backend,\n handlers: [\n http.get(baseUrl, async ({ request }) =>\n respond(await backend.list(new URL(request.url).searchParams)),\n ),\n http.get(`${baseUrl}/:id`, async ({ params }) =>\n respond(await backend.get(String(params['id']))),\n ),\n ],\n };\n};\n\n/**\n * The configuration lookup every entifix frontend makes before it can issue a\n * single entity request: the REST adapters resolve their base URL through it.\n *\n * Stubbing the entity endpoint without this one leaves the page waiting\n * forever and the spec passing vacuously, so it is a first-class fixture rather\n * than something each suite remembers to add.\n */\nexport const configurationHandler = (\n url: string,\n configuration: ConfigurationPlain,\n): RequestHandler => http.get(url, () => HttpResponse.json(configuration));\n\nexport type { RequestHandler } from 'msw';\nexport { http, HttpResponse } from 'msw';\n"],"names":["http","HttpResponse","makeEntityBackend","entityBackendHandlers","entityConstructor","baseUrl","options","backend","respond","status","body","json","handlers","get","request","list","URL","url","searchParams","params","String","configurationHandler","configuration"],"mappings":";AAEA,SAASA,IAAI,EAAEC,YAAY,QAA6B,MAAM;AAE9D,SAIEC,iBAAiB,QACZ,sBAAsB;AAO7B;;;;;;;CAOC,GACD,OAAO,MAAMC,wBAAwB,CACnCC;QACA,EAAEC,OAAO,EAA2C,OAAtCC;;;IAEd,MAAMC,UAAUL,kBAAkBE,mBAAmBE;IAErD,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAME,UAAU,CAAC,EAAEC,MAAM,EAAEC,IAAI,EAAmB,GAChDT,aAAaU,IAAI,CAACD,MAAiC;YAAED;QAAO;IAE9D,OAAO;QACLF;QACAK,UAAU;YACRZ,KAAKa,GAAG,CAACR,SAAS,OAAO,EAAES,OAAO,EAAE,GAClCN,QAAQ,MAAMD,QAAQQ,IAAI,CAAC,IAAIC,IAAIF,QAAQG,GAAG,EAAEC,YAAY;
|
|
1
|
+
{"version":3,"sources":["../../src/fixtures/entity-handlers.ts"],"sourcesContent":["import type { ConfigurationPlain } from '@entifix/core';\nimport type { Entity, EntityConstructor } from '@entifix/core';\nimport { http, HttpResponse, type RequestHandler } from 'msw';\n\nimport {\n type BackendResponse,\n type EntityBackend,\n type EntityBackendOptions,\n makeEntityBackend,\n} from './entity-backend.js';\n\nexport interface EntityBackendHandlerOptions extends EntityBackendOptions {\n /** The collection endpoint, e.g. `http://localhost:3101/api/product-brand`. */\n baseUrl: string;\n}\n\n/**\n * Puts an {@link makeEntityBackend} behind msw handlers, so a browser under\n * Playwright talks to the real query pipeline over the real wire format.\n *\n * The backend is returned alongside the handlers because a spec usually wants\n * both: the handlers to install, and the backend to reseed or to break with\n * `backend.db.failWith(...)`.\n */\nexport const entityBackendHandlers = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n { baseUrl, ...options }: EntityBackendHandlerOptions,\n): { handlers: RequestHandler[]; backend: EntityBackend } => {\n const backend = makeEntityBackend(entityConstructor, options);\n\n // Every body is an envelope or an error record — JSON either way. The cast is\n // the one place the backend's transport-free `unknown` meets msw's body type.\n const respond = ({ status, body }: BackendResponse) =>\n HttpResponse.json(body as Record<string, unknown>, { status });\n\n return {\n backend,\n handlers: [\n http.get(baseUrl, async ({ request }) =>\n respond(await backend.list(new URL(request.url).searchParams)),\n ),\n // Before `/:id`, which would otherwise take `$facets` for an id.\n http.get(`${baseUrl}/$facets`, async ({ request }) =>\n respond(await backend.facets(new URL(request.url).searchParams)),\n ),\n http.get(`${baseUrl}/:id`, async ({ params }) =>\n respond(await backend.get(String(params['id']))),\n ),\n ],\n };\n};\n\n/**\n * The configuration lookup every entifix frontend makes before it can issue a\n * single entity request: the REST adapters resolve their base URL through it.\n *\n * Stubbing the entity endpoint without this one leaves the page waiting\n * forever and the spec passing vacuously, so it is a first-class fixture rather\n * than something each suite remembers to add.\n */\nexport const configurationHandler = (\n url: string,\n configuration: ConfigurationPlain,\n): RequestHandler => http.get(url, () => HttpResponse.json(configuration));\n\nexport type { RequestHandler } from 'msw';\nexport { http, HttpResponse } from 'msw';\n"],"names":["http","HttpResponse","makeEntityBackend","entityBackendHandlers","entityConstructor","baseUrl","options","backend","respond","status","body","json","handlers","get","request","list","URL","url","searchParams","facets","params","String","configurationHandler","configuration"],"mappings":";AAEA,SAASA,IAAI,EAAEC,YAAY,QAA6B,MAAM;AAE9D,SAIEC,iBAAiB,QACZ,sBAAsB;AAO7B;;;;;;;CAOC,GACD,OAAO,MAAMC,wBAAwB,CACnCC;QACA,EAAEC,OAAO,EAA2C,OAAtCC;;;IAEd,MAAMC,UAAUL,kBAAkBE,mBAAmBE;IAErD,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAME,UAAU,CAAC,EAAEC,MAAM,EAAEC,IAAI,EAAmB,GAChDT,aAAaU,IAAI,CAACD,MAAiC;YAAED;QAAO;IAE9D,OAAO;QACLF;QACAK,UAAU;YACRZ,KAAKa,GAAG,CAACR,SAAS,OAAO,EAAES,OAAO,EAAE,GAClCN,QAAQ,MAAMD,QAAQQ,IAAI,CAAC,IAAIC,IAAIF,QAAQG,GAAG,EAAEC,YAAY;YAE9D,iEAAiE;YACjElB,KAAKa,GAAG,CAAC,GAAGR,QAAQ,QAAQ,CAAC,EAAE,OAAO,EAAES,OAAO,EAAE,GAC/CN,QAAQ,MAAMD,QAAQY,MAAM,CAAC,IAAIH,IAAIF,QAAQG,GAAG,EAAEC,YAAY;YAEhElB,KAAKa,GAAG,CAAC,GAAGR,QAAQ,IAAI,CAAC,EAAE,OAAO,EAAEe,MAAM,EAAE,GAC1CZ,QAAQ,MAAMD,QAAQM,GAAG,CAACQ,OAAOD,MAAM,CAAC,KAAK;SAEhD;IACH;AACF,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAME,uBAAuB,CAClCL,KACAM,gBACmBvB,KAAKa,GAAG,CAACI,KAAK,IAAMhB,aAAaU,IAAI,CAACY,gBAAgB;AAG3E,SAASvB,IAAI,EAAEC,YAAY,QAAQ,MAAM"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/testing-e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "End-to-end testing with one set of journeys and two profiles: mocked backends by default, real infrastructure on demand.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@swc/helpers": "0.5.23",
|
|
70
|
-
"@entifix/
|
|
71
|
-
"@entifix/
|
|
72
|
-
"@entifix/
|
|
70
|
+
"@entifix/business": "0.9.0",
|
|
71
|
+
"@entifix/testing-unit": "0.9.0",
|
|
72
|
+
"@entifix/core": "0.9.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@nx/devkit": ">=23 <24",
|
|
@@ -80,9 +80,9 @@
|
|
|
80
80
|
"msw": ">=3",
|
|
81
81
|
"@msw/playwright": ">=0.7",
|
|
82
82
|
"vitest": "*",
|
|
83
|
-
"@entifix/mongo": "0.
|
|
84
|
-
"@entifix/redis": "0.
|
|
85
|
-
"@entifix/amqp": "0.
|
|
83
|
+
"@entifix/mongo": "0.9.0",
|
|
84
|
+
"@entifix/redis": "0.9.0",
|
|
85
|
+
"@entifix/amqp": "0.9.0"
|
|
86
86
|
},
|
|
87
87
|
"peerDependenciesMeta": {
|
|
88
88
|
"@msw/playwright": {
|
|
@@ -120,8 +120,8 @@
|
|
|
120
120
|
"@nx/devkit": "23.3.0",
|
|
121
121
|
"@nx/playwright": "23.3.0",
|
|
122
122
|
"effect": "4.0.5",
|
|
123
|
-
"@entifix/
|
|
124
|
-
"@entifix/
|
|
125
|
-
"@entifix/redis": "0.
|
|
123
|
+
"@entifix/mongo": "0.9.0",
|
|
124
|
+
"@entifix/amqp": "0.9.0",
|
|
125
|
+
"@entifix/redis": "0.9.0"
|
|
126
126
|
}
|
|
127
127
|
}
|