@entifix/testing-unit 0.1.0-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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/builders/entity-builder.d.ts +26 -0
  4. package/dist/builders/entity-builder.d.ts.map +1 -0
  5. package/dist/builders/entity-builder.js +30 -0
  6. package/dist/builders/entity-builder.js.map +1 -0
  7. package/dist/contracts/entity-repository.contract.d.ts +36 -0
  8. package/dist/contracts/entity-repository.contract.d.ts.map +1 -0
  9. package/dist/contracts/entity-repository.contract.js +273 -0
  10. package/dist/contracts/entity-repository.contract.js.map +1 -0
  11. package/dist/contracts/index.d.ts +3 -0
  12. package/dist/contracts/index.d.ts.map +1 -0
  13. package/dist/contracts/index.js +4 -0
  14. package/dist/contracts/index.js.map +1 -0
  15. package/dist/contracts/transaction-ports.contract.d.ts +44 -0
  16. package/dist/contracts/transaction-ports.contract.d.ts.map +1 -0
  17. package/dist/contracts/transaction-ports.contract.js +203 -0
  18. package/dist/contracts/transaction-ports.contract.js.map +1 -0
  19. package/dist/doubles/configuration.d.ts +36 -0
  20. package/dist/doubles/configuration.d.ts.map +1 -0
  21. package/dist/doubles/configuration.js +31 -0
  22. package/dist/doubles/configuration.js.map +1 -0
  23. package/dist/doubles/entity-repository.d.ts +24 -0
  24. package/dist/doubles/entity-repository.d.ts.map +1 -0
  25. package/dist/doubles/entity-repository.js +159 -0
  26. package/dist/doubles/entity-repository.js.map +1 -0
  27. package/dist/doubles/link-resolver.d.ts +17 -0
  28. package/dist/doubles/link-resolver.d.ts.map +1 -0
  29. package/dist/doubles/link-resolver.js +34 -0
  30. package/dist/doubles/link-resolver.js.map +1 -0
  31. package/dist/doubles/transaction-ports.d.ts +70 -0
  32. package/dist/doubles/transaction-ports.d.ts.map +1 -0
  33. package/dist/doubles/transaction-ports.js +145 -0
  34. package/dist/doubles/transaction-ports.js.map +1 -0
  35. package/dist/drivers/fake-amqp.d.ts +91 -0
  36. package/dist/drivers/fake-amqp.d.ts.map +1 -0
  37. package/dist/drivers/fake-amqp.js +164 -0
  38. package/dist/drivers/fake-amqp.js.map +1 -0
  39. package/dist/drivers/fake-mongo.d.ts +59 -0
  40. package/dist/drivers/fake-mongo.d.ts.map +1 -0
  41. package/dist/drivers/fake-mongo.js +603 -0
  42. package/dist/drivers/fake-mongo.js.map +1 -0
  43. package/dist/drivers/fake-redis.d.ts +32 -0
  44. package/dist/drivers/fake-redis.d.ts.map +1 -0
  45. package/dist/drivers/fake-redis.js +164 -0
  46. package/dist/drivers/fake-redis.js.map +1 -0
  47. package/dist/drivers/fake-sql.d.ts +58 -0
  48. package/dist/drivers/fake-sql.d.ts.map +1 -0
  49. package/dist/drivers/fake-sql.js +102 -0
  50. package/dist/drivers/fake-sql.js.map +1 -0
  51. package/dist/drivers/index.d.ts +5 -0
  52. package/dist/drivers/index.d.ts.map +1 -0
  53. package/dist/drivers/index.js +6 -0
  54. package/dist/drivers/index.js.map +1 -0
  55. package/dist/effect/run.d.ts +34 -0
  56. package/dist/effect/run.d.ts.map +1 -0
  57. package/dist/effect/run.js +48 -0
  58. package/dist/effect/run.js.map +1 -0
  59. package/dist/http/entity-handlers.d.ts +37 -0
  60. package/dist/http/entity-handlers.d.ts.map +1 -0
  61. package/dist/http/entity-handlers.js +92 -0
  62. package/dist/http/entity-handlers.js.map +1 -0
  63. package/dist/http/index.d.ts +3 -0
  64. package/dist/http/index.d.ts.map +1 -0
  65. package/dist/http/index.js +4 -0
  66. package/dist/http/index.js.map +1 -0
  67. package/dist/http/server.d.ts +19 -0
  68. package/dist/http/server.d.ts.map +1 -0
  69. package/dist/http/server.js +31 -0
  70. package/dist/http/server.js.map +1 -0
  71. package/dist/index.d.ts +7 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/react/index.d.ts +3 -0
  76. package/dist/react/index.d.ts.map +1 -0
  77. package/dist/react/index.js +4 -0
  78. package/dist/react/index.js.map +1 -0
  79. package/dist/react/render.d.ts +30 -0
  80. package/dist/react/render.d.ts.map +1 -0
  81. package/dist/react/render.js +38 -0
  82. package/dist/react/render.js.map +1 -0
  83. package/dist/react/ui-preferences.d.ts +17 -0
  84. package/dist/react/ui-preferences.d.ts.map +1 -0
  85. package/dist/react/ui-preferences.js +29 -0
  86. package/dist/react/ui-preferences.js.map +1 -0
  87. package/package.json +122 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/effect/run.ts"],"sourcesContent":["import type { ConfigurationRepositoryTag } from '@entifix/business';\nimport { Effect, Exit, type Layer } from 'effect';\n\nimport { stubConfigurationLayer } from '../doubles/configuration.js';\n\n/**\n * Runs an Effect that still requires `TContext`, providing it from `layer`.\n *\n * Use-cases are `Effect.gen` blocks yielding Context tags; every spec would\n * otherwise repeat the same `Effect.runPromise(uc.pipe(Effect.provide(…)))`\n * incantation.\n */\nexport const runUC = <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, layer));\n\n/**\n * Runs an Effect expected to fail and returns its typed error.\n *\n * Asserting on the error value beats asserting that a promise rejected: it is\n * the difference between \"something went wrong\" and \"it failed with the\n * `EntifixLockError` that the facade turns into a 409\".\n */\nexport const expectFailure = async <TValue, TError, TContext>(\n effect: Effect.Effect<TValue, TError, TContext>,\n layer: Layer.Layer<TContext>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(Effect.provide(effect, layer));\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n\n const failure = exit.cause;\n if (failure._tag === 'Fail') {\n return failure.error;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(failure)}`,\n );\n};\n\n/**\n * Runs a repository Effect.\n *\n * Every `EntityRepository` method declares `ConfigurationRepositoryTag` in its\n * requirement channel — the REST-backed adapters resolve their endpoints\n * through it — so even a repository that needs no configuration has to be given\n * one. `configuration` supplies it for the adapters that do.\n */\nexport const runRepository = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<TValue> => Effect.runPromise(Effect.provide(effect, configuration));\n\n/** {@link runRepository}, for an Effect expected to fail. */\nexport const runRepositoryExit = <TValue, TError>(\n effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>,\n configuration: Layer.Layer<ConfigurationRepositoryTag> = stubConfigurationLayer(),\n): Promise<Exit.Exit<TValue, TError>> =>\n Effect.runPromiseExit(Effect.provide(effect, configuration));\n\n/** Runs an Effect that requires nothing, returning its value. */\nexport const run = <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TValue> => Effect.runPromise(effect);\n\n/** Runs an Effect that requires nothing and returns its typed error. */\nexport const runFailure = async <TValue, TError>(\n effect: Effect.Effect<TValue, TError>,\n): Promise<TError> => {\n const exit = await Effect.runPromiseExit(effect);\n\n if (Exit.isSuccess(exit)) {\n throw new Error(\n `Expected the effect to fail, but it succeeded with: ${JSON.stringify(\n exit.value,\n )}`,\n );\n }\n if (exit.cause._tag === 'Fail') {\n return exit.cause.error;\n }\n throw new Error(\n `Expected a typed failure, but the effect died with: ${String(exit.cause)}`,\n );\n};\n"],"names":["Effect","Exit","stubConfigurationLayer","runUC","effect","layer","runPromise","provide","expectFailure","exit","runPromiseExit","isSuccess","Error","JSON","stringify","value","failure","cause","_tag","error","String","runRepository","configuration","runRepositoryExit","run","runFailure"],"mappings":"AACA,SAASA,MAAM,EAAEC,IAAI,QAAoB,SAAS;AAElD,SAASC,sBAAsB,QAAQ,8BAA8B;AAErE;;;;;;CAMC,GACD,OAAO,MAAMC,QAAQ,CACnBC,QACAC,QACoBL,OAAOM,UAAU,CAACN,OAAOO,OAAO,CAACH,QAAQC,QAAQ;AAEvE;;;;;;CAMC,GACD,OAAO,MAAMG,gBAAgB,OAC3BJ,QACAC;IAEA,MAAMI,OAAO,MAAMT,OAAOU,cAAc,CAACV,OAAOO,OAAO,CAACH,QAAQC;IAEhE,IAAIJ,KAAKU,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IAEA,MAAMC,UAAUP,KAAKQ,KAAK;IAC1B,IAAID,QAAQE,IAAI,KAAK,QAAQ;QAC3B,OAAOF,QAAQG,KAAK;IACtB;IACA,MAAM,IAAIP,MACR,CAAC,oDAAoD,EAAEQ,OAAOJ,UAAU;AAE5E,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMK,gBAAgB,CAC3BjB,QACAkB,gBAAyDpB,wBAAwB,GAC7DF,OAAOM,UAAU,CAACN,OAAOO,OAAO,CAACH,QAAQkB,gBAAgB;AAE/E,2DAA2D,GAC3D,OAAO,MAAMC,oBAAoB,CAC/BnB,QACAkB,gBAAyDpB,wBAAwB,GAEjFF,OAAOU,cAAc,CAACV,OAAOO,OAAO,CAACH,QAAQkB,gBAAgB;AAE/D,+DAA+D,GAC/D,OAAO,MAAME,MAAM,CACjBpB,SACoBJ,OAAOM,UAAU,CAACF,QAAQ;AAEhD,sEAAsE,GACtE,OAAO,MAAMqB,aAAa,OACxBrB;IAEA,MAAMK,OAAO,MAAMT,OAAOU,cAAc,CAACN;IAEzC,IAAIH,KAAKU,SAAS,CAACF,OAAO;QACxB,MAAM,IAAIG,MACR,CAAC,oDAAoD,EAAEC,KAAKC,SAAS,CACnEL,KAAKM,KAAK,GACT;IAEP;IACA,IAAIN,KAAKQ,KAAK,CAACC,IAAI,KAAK,QAAQ;QAC9B,OAAOT,KAAKQ,KAAK,CAACE,KAAK;IACzB;IACA,MAAM,IAAIP,MACR,CAAC,oDAAoD,EAAEQ,OAAOX,KAAKQ,KAAK,GAAG;AAE/E,EAAE"}
@@ -0,0 +1,37 @@
1
+ import { type Entity, type EntityConstructor, type EntityId } from '@entifix/core';
2
+ import { type RequestHandler } from 'msw';
3
+ export interface EntityRestHandlerOptions<TEntity extends Entity> {
4
+ /** The collection endpoint, e.g. `http://service/api/product`. */
5
+ baseUrl: string;
6
+ /** The rows the fake service serves. Mutated in place by writes. */
7
+ data?: TEntity[];
8
+ }
9
+ /**
10
+ * MSW handlers for the five REST operations the entity adapters issue, speaking
11
+ * the real entifix envelope.
12
+ *
13
+ * Responses are built with `makeEntityEnvelope`/`makeEntityPageEnvelope` — the
14
+ * production serializer — so a fixture cannot drift from the wire format the
15
+ * adapters parse. Hand-written JSON literals in specs are exactly how that
16
+ * drift starts.
17
+ */
18
+ export declare const entityRestHandlers: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, { baseUrl, data }: EntityRestHandlerOptions<TEntity>) => RequestHandler[];
19
+ /** A single entity response, for specs that need one endpoint only. */
20
+ export declare const respondWithEntity: <TEntity extends Entity>(url: string, entityConstructor: EntityConstructor<TEntity>, instance: TEntity) => RequestHandler;
21
+ /**
22
+ * The failure handlers. Adapters map transport and shape failures onto
23
+ * `EntifixError`s, and those branches are only reachable from here.
24
+ */
25
+ export declare const respondWith500: (url: string, method?: "get" | "post" | "put" | "delete") => RequestHandler;
26
+ export declare const respondWith404: (url: string, method?: "get" | "post" | "put" | "delete") => RequestHandler;
27
+ /** Answers 200 with a body that is not an envelope at all. */
28
+ export declare const respondWithMalformedEnvelope: (url: string, method?: "get" | "post" | "put" | "delete", body?: Record<string, unknown>) => RequestHandler;
29
+ /** Answers 200 with a body that is not JSON. */
30
+ export declare const respondWithNonJson: (url: string, method?: "get" | "post" | "put" | "delete") => RequestHandler;
31
+ /** Fails the request at the transport level, as a dropped connection would. */
32
+ export declare const respondWithNetworkError: (url: string, method?: "get" | "post" | "put" | "delete") => RequestHandler;
33
+ export type { RequestHandler } from 'msw';
34
+ export { http, HttpResponse } from 'msw';
35
+ /** Re-exported so `EntityId` stays available to handler authors. */
36
+ export type { EntityId };
37
+ //# sourceMappingURL=entity-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-handlers.d.ts","sourceRoot":"","sources":["../../src/http/entity-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EAGd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE9D,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,MAAM,EACvD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,mBAAwB,wBAAwB,CAAC,OAAO,CAAC,KACxD,cAAc,EAyEhB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,GAAI,OAAO,SAAS,MAAM,EACtD,KAAK,MAAM,EACX,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,UAAU,OAAO,KAChB,cAGA,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAC+D,CAAC;AAEnE,eAAO,MAAM,cAAc,GACzB,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAC+D,CAAC;AAEnE,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,GACvC,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,EACjD,OAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAA0C,KACrE,cAAkE,CAAC;AAEtE,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,GAC7B,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAIA,CAAC;AAEJ,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,GAClC,KAAK,MAAM,EACX,SAAQ,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,KAChD,cAA+D,CAAC;AAEnE,YAAY,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AAEzC,oEAAoE;AACpE,YAAY,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,92 @@
1
+ import { makeEntityEnvelope, makeEntityPageEnvelope } from '@entifix/core';
2
+ import { http, HttpResponse } from 'msw';
3
+ /**
4
+ * MSW handlers for the five REST operations the entity adapters issue, speaking
5
+ * the real entifix envelope.
6
+ *
7
+ * Responses are built with `makeEntityEnvelope`/`makeEntityPageEnvelope` — the
8
+ * production serializer — so a fixture cannot drift from the wire format the
9
+ * adapters parse. Hand-written JSON literals in specs are exactly how that
10
+ * drift starts.
11
+ */ export const entityRestHandlers = (entityConstructor, { baseUrl, data = [] })=>{
12
+ const rows = data;
13
+ const findById = (id)=>rows.find((row)=>String(row.id) === id);
14
+ return [
15
+ http.get(baseUrl, ({ request })=>{
16
+ var _url_searchParams_get, _url_searchParams_get1;
17
+ const url = new URL(request.url);
18
+ const page = Number((_url_searchParams_get = url.searchParams.get('page')) != null ? _url_searchParams_get : '1');
19
+ const pageSize = Number((_url_searchParams_get1 = url.searchParams.get('pageSize')) != null ? _url_searchParams_get1 : '10');
20
+ const start = (page - 1) * pageSize;
21
+ return HttpResponse.json(makeEntityPageEnvelope(entityConstructor, {
22
+ items: rows.slice(start, start + pageSize),
23
+ total: rows.length,
24
+ request: {
25
+ page,
26
+ pageSize
27
+ }
28
+ }));
29
+ }),
30
+ http.get(`${baseUrl}/:id`, ({ params })=>{
31
+ const found = findById(String(params['id']));
32
+ return found === undefined ? new HttpResponse(null, {
33
+ status: 404
34
+ }) : HttpResponse.json(makeEntityEnvelope(entityConstructor, found));
35
+ }),
36
+ http.post(baseUrl, async ({ request })=>{
37
+ var _body_data_id;
38
+ const body = await request.json();
39
+ // The service is what mints an id on create; echoing the request back
40
+ // unchanged would hide that the adapter must read the response.
41
+ const created = Object.assign(new entityConstructor(), body.data, {
42
+ id: (_body_data_id = body.data['id']) != null ? _body_data_id : `generated-${rows.length + 1}`
43
+ });
44
+ rows.push(created);
45
+ return HttpResponse.json(makeEntityEnvelope(entityConstructor, created), {
46
+ status: 201
47
+ });
48
+ }),
49
+ http.put(`${baseUrl}/:id`, async ({ params, request })=>{
50
+ const body = await request.json();
51
+ const id = String(params['id']);
52
+ const index = rows.findIndex((row)=>String(row.id) === id);
53
+ const updated = Object.assign(new entityConstructor(), body.data);
54
+ if (index === -1) {
55
+ rows.push(updated);
56
+ } else {
57
+ rows[index] = updated;
58
+ }
59
+ return HttpResponse.json(makeEntityEnvelope(entityConstructor, updated));
60
+ }),
61
+ // A delete answers with the removed entity's envelope rather than a bare
62
+ // `204`: the shared fetch client always parses the response as JSON, so an
63
+ // empty body would fail that parse — which is what the services do too.
64
+ http.delete(`${baseUrl}/:id`, ({ params })=>{
65
+ const id = String(params['id']);
66
+ const index = rows.findIndex((row)=>String(row.id) === id);
67
+ const [removed] = index === -1 ? [] : rows.splice(index, 1);
68
+ return HttpResponse.json(makeEntityEnvelope(entityConstructor, removed != null ? removed : new entityConstructor()));
69
+ })
70
+ ];
71
+ };
72
+ /** A single entity response, for specs that need one endpoint only. */ export const respondWithEntity = (url, entityConstructor, instance)=>http.get(url, ()=>HttpResponse.json(makeEntityEnvelope(entityConstructor, instance)));
73
+ /**
74
+ * The failure handlers. Adapters map transport and shape failures onto
75
+ * `EntifixError`s, and those branches are only reachable from here.
76
+ */ export const respondWith500 = (url, method = 'get')=>http[method](url, ()=>new HttpResponse(null, {
77
+ status: 500
78
+ }));
79
+ export const respondWith404 = (url, method = 'get')=>http[method](url, ()=>new HttpResponse(null, {
80
+ status: 404
81
+ }));
82
+ /** Answers 200 with a body that is not an envelope at all. */ export const respondWithMalformedEnvelope = (url, method = 'get', body = {
83
+ id: 'raw',
84
+ name: 'not an envelope'
85
+ })=>http[method](url, ()=>HttpResponse.json(body));
86
+ /** Answers 200 with a body that is not JSON. */ export const respondWithNonJson = (url, method = 'get')=>http[method](url, ()=>new HttpResponse('<html>nope</html>', {
87
+ status: 200
88
+ }));
89
+ /** Fails the request at the transport level, as a dropped connection would. */ export const respondWithNetworkError = (url, method = 'get')=>http[method](url, ()=>HttpResponse.error());
90
+ export { http, HttpResponse } from 'msw';
91
+
92
+ //# sourceMappingURL=entity-handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/http/entity-handlers.ts"],"sourcesContent":["import {\n type Entity,\n type EntityConstructor,\n type EntityId,\n makeEntityEnvelope,\n makeEntityPageEnvelope,\n} from '@entifix/core';\nimport { http, HttpResponse, type RequestHandler } from 'msw';\n\nexport interface EntityRestHandlerOptions<TEntity extends Entity> {\n /** The collection endpoint, e.g. `http://service/api/product`. */\n baseUrl: string;\n /** The rows the fake service serves. Mutated in place by writes. */\n data?: TEntity[];\n}\n\n/**\n * MSW handlers for the five REST operations the entity adapters issue, speaking\n * the real entifix envelope.\n *\n * Responses are built with `makeEntityEnvelope`/`makeEntityPageEnvelope` — the\n * production serializer — so a fixture cannot drift from the wire format the\n * adapters parse. Hand-written JSON literals in specs are exactly how that\n * drift starts.\n */\nexport const entityRestHandlers = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n { baseUrl, data = [] }: EntityRestHandlerOptions<TEntity>,\n): RequestHandler[] => {\n const rows = data;\n\n const findById = (id: string): TEntity | undefined =>\n rows.find(row => String(row.id) === id);\n\n return [\n http.get(baseUrl, ({ request }) => {\n const url = new URL(request.url);\n const page = Number(url.searchParams.get('page') ?? '1');\n const pageSize = Number(url.searchParams.get('pageSize') ?? '10');\n const start = (page - 1) * pageSize;\n\n return HttpResponse.json(\n makeEntityPageEnvelope(entityConstructor, {\n items: rows.slice(start, start + pageSize),\n total: rows.length,\n request: { page, pageSize },\n }),\n );\n }),\n\n http.get(`${baseUrl}/:id`, ({ params }) => {\n const found = findById(String(params['id']));\n return found === undefined\n ? new HttpResponse(null, { status: 404 })\n : HttpResponse.json(makeEntityEnvelope(entityConstructor, found));\n }),\n\n http.post(baseUrl, async ({ request }) => {\n const body = (await request.json()) as { data: Record<string, unknown> };\n // The service is what mints an id on create; echoing the request back\n // unchanged would hide that the adapter must read the response.\n const created = Object.assign(new entityConstructor(), body.data, {\n id: body.data['id'] ?? `generated-${rows.length + 1}`,\n }) as TEntity;\n rows.push(created);\n return HttpResponse.json(makeEntityEnvelope(entityConstructor, created), {\n status: 201,\n });\n }),\n\n http.put(`${baseUrl}/:id`, async ({ params, request }) => {\n const body = (await request.json()) as { data: Record<string, unknown> };\n const id = String(params['id']);\n const index = rows.findIndex(row => String(row.id) === id);\n const updated = Object.assign(\n new entityConstructor(),\n body.data,\n ) as TEntity;\n if (index === -1) {\n rows.push(updated);\n } else {\n rows[index] = updated;\n }\n return HttpResponse.json(makeEntityEnvelope(entityConstructor, updated));\n }),\n\n // A delete answers with the removed entity's envelope rather than a bare\n // `204`: the shared fetch client always parses the response as JSON, so an\n // empty body would fail that parse — which is what the services do too.\n http.delete(`${baseUrl}/:id`, ({ params }) => {\n const id = String(params['id']);\n const index = rows.findIndex(row => String(row.id) === id);\n const [removed] = index === -1 ? [] : rows.splice(index, 1);\n return HttpResponse.json(\n makeEntityEnvelope(\n entityConstructor,\n removed ?? (new entityConstructor() as TEntity),\n ),\n );\n }),\n ];\n};\n\n/** A single entity response, for specs that need one endpoint only. */\nexport const respondWithEntity = <TEntity extends Entity>(\n url: string,\n entityConstructor: EntityConstructor<TEntity>,\n instance: TEntity,\n): RequestHandler =>\n http.get(url, () =>\n HttpResponse.json(makeEntityEnvelope(entityConstructor, instance)),\n );\n\n/**\n * The failure handlers. Adapters map transport and shape failures onto\n * `EntifixError`s, and those branches are only reachable from here.\n */\nexport const respondWith500 = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](url, () => new HttpResponse(null, { status: 500 }));\n\nexport const respondWith404 = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](url, () => new HttpResponse(null, { status: 404 }));\n\n/** Answers 200 with a body that is not an envelope at all. */\nexport const respondWithMalformedEnvelope = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n body: Record<string, unknown> = { id: 'raw', name: 'not an envelope' },\n): RequestHandler => http[method](url, () => HttpResponse.json(body));\n\n/** Answers 200 with a body that is not JSON. */\nexport const respondWithNonJson = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler =>\n http[method](\n url,\n () => new HttpResponse('<html>nope</html>', { status: 200 }),\n );\n\n/** Fails the request at the transport level, as a dropped connection would. */\nexport const respondWithNetworkError = (\n url: string,\n method: 'get' | 'post' | 'put' | 'delete' = 'get',\n): RequestHandler => http[method](url, () => HttpResponse.error());\n\nexport type { RequestHandler } from 'msw';\nexport { http, HttpResponse } from 'msw';\n\n/** Re-exported so `EntityId` stays available to handler authors. */\nexport type { EntityId };\n"],"names":["makeEntityEnvelope","makeEntityPageEnvelope","http","HttpResponse","entityRestHandlers","entityConstructor","baseUrl","data","rows","findById","id","find","row","String","get","request","url","URL","page","Number","searchParams","pageSize","start","json","items","slice","total","length","params","found","undefined","status","post","body","created","Object","assign","push","put","index","findIndex","updated","delete","removed","splice","respondWithEntity","instance","respondWith500","method","respondWith404","respondWithMalformedEnvelope","name","respondWithNonJson","respondWithNetworkError","error"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export * from './entity-handlers.js';
2
+ export * from './server.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './entity-handlers.js';
2
+ export * from './server.js';
3
+
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/http/index.ts"],"sourcesContent":["export * from './entity-handlers.js';\nexport * from './server.js';\n"],"names":[],"mappings":"AAAA,cAAc,uBAAuB;AACrC,cAAc,cAAc"}
@@ -0,0 +1,19 @@
1
+ import { type RequestHandler } from 'msw';
2
+ import { setupServer } from 'msw/node';
3
+ /** What `setupServer` actually returns, so the type cannot drift from msw. */
4
+ export type EntifixMockServer = ReturnType<typeof setupServer>;
5
+ /**
6
+ * Starts an MSW server for the current test file and wires its lifecycle.
7
+ *
8
+ * `onUnhandledRequest: 'error'` is the important part: a request nobody stubbed
9
+ * fails the test instead of quietly escaping to the network. A test that passes
10
+ * because it silently talked to nothing is worse than one that fails.
11
+ *
12
+ * ```ts
13
+ * const server = setupEntifixServer(...entityRestHandlers(Product, { … }));
14
+ * // inside a test, to override for one case:
15
+ * server.use(respondWith500('http://service/api/product'));
16
+ * ```
17
+ */
18
+ export declare const setupEntifixServer: (...handlers: RequestHandler[]) => EntifixMockServer;
19
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/http/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGvC,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAC7B,GAAG,UAAU,cAAc,EAAE,KAC5B,iBAcF,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { setupServer } from 'msw/node';
2
+ import { afterAll, afterEach, beforeAll } from 'vitest';
3
+ /**
4
+ * Starts an MSW server for the current test file and wires its lifecycle.
5
+ *
6
+ * `onUnhandledRequest: 'error'` is the important part: a request nobody stubbed
7
+ * fails the test instead of quietly escaping to the network. A test that passes
8
+ * because it silently talked to nothing is worse than one that fails.
9
+ *
10
+ * ```ts
11
+ * const server = setupEntifixServer(...entityRestHandlers(Product, { … }));
12
+ * // inside a test, to override for one case:
13
+ * server.use(respondWith500('http://service/api/product'));
14
+ * ```
15
+ */ export const setupEntifixServer = (...handlers)=>{
16
+ const server = setupServer(...handlers);
17
+ beforeAll(()=>{
18
+ server.listen({
19
+ onUnhandledRequest: 'error'
20
+ });
21
+ });
22
+ afterEach(()=>{
23
+ server.resetHandlers();
24
+ });
25
+ afterAll(()=>{
26
+ server.close();
27
+ });
28
+ return server;
29
+ };
30
+
31
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/http/server.ts"],"sourcesContent":["import { type RequestHandler } from 'msw';\nimport { setupServer } from 'msw/node';\nimport { afterAll, afterEach, beforeAll } from 'vitest';\n\n/** What `setupServer` actually returns, so the type cannot drift from msw. */\nexport type EntifixMockServer = ReturnType<typeof setupServer>;\n\n/**\n * Starts an MSW server for the current test file and wires its lifecycle.\n *\n * `onUnhandledRequest: 'error'` is the important part: a request nobody stubbed\n * fails the test instead of quietly escaping to the network. A test that passes\n * because it silently talked to nothing is worse than one that fails.\n *\n * ```ts\n * const server = setupEntifixServer(...entityRestHandlers(Product, { … }));\n * // inside a test, to override for one case:\n * server.use(respondWith500('http://service/api/product'));\n * ```\n */\nexport const setupEntifixServer = (\n ...handlers: RequestHandler[]\n): EntifixMockServer => {\n const server = setupServer(...handlers);\n\n beforeAll(() => {\n server.listen({ onUnhandledRequest: 'error' });\n });\n afterEach(() => {\n server.resetHandlers();\n });\n afterAll(() => {\n server.close();\n });\n\n return server;\n};\n"],"names":["setupServer","afterAll","afterEach","beforeAll","setupEntifixServer","handlers","server","listen","onUnhandledRequest","resetHandlers","close"],"mappings":"AACA,SAASA,WAAW,QAAQ,WAAW;AACvC,SAASC,QAAQ,EAAEC,SAAS,EAAEC,SAAS,QAAQ,SAAS;AAKxD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,qBAAqB,CAChC,GAAGC;IAEH,MAAMC,SAASN,eAAeK;IAE9BF,UAAU;QACRG,OAAOC,MAAM,CAAC;YAAEC,oBAAoB;QAAQ;IAC9C;IACAN,UAAU;QACRI,OAAOG,aAAa;IACtB;IACAR,SAAS;QACPK,OAAOI,KAAK;IACd;IAEA,OAAOJ;AACT,EAAE"}
@@ -0,0 +1,7 @@
1
+ export * from './builders/entity-builder.js';
2
+ export * from './doubles/configuration.js';
3
+ export * from './doubles/entity-repository.js';
4
+ export * from './doubles/link-resolver.js';
5
+ export * from './doubles/transaction-ports.js';
6
+ export * from './effect/run.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './builders/entity-builder.js';
2
+ export * from './doubles/configuration.js';
3
+ export * from './doubles/entity-repository.js';
4
+ export * from './doubles/link-resolver.js';
5
+ export * from './doubles/transaction-ports.js';
6
+ export * from './effect/run.js';
7
+
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './builders/entity-builder.js';\nexport * from './doubles/configuration.js';\nexport * from './doubles/entity-repository.js';\nexport * from './doubles/link-resolver.js';\nexport * from './doubles/transaction-ports.js';\nexport * from './effect/run.js';\n"],"names":[],"mappings":"AAAA,cAAc,+BAA+B;AAC7C,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,kBAAkB"}
@@ -0,0 +1,3 @@
1
+ export * from './render.js';
2
+ export * from './ui-preferences.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.tsx"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './render.js';
2
+ export * from './ui-preferences.js';
3
+
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.tsx"],"sourcesContent":["export * from './render.js';\nexport * from './ui-preferences.js';\n"],"names":[],"mappings":"AAAA,cAAc,cAAc;AAC5B,cAAc,sBAAsB"}
@@ -0,0 +1,30 @@
1
+ import { type UiPreferencesState } from '@entifix/react-controls';
2
+ import { type RenderOptions, type RenderResult } from '@testing-library/react';
3
+ import type { Context, ReactElement } from 'react';
4
+ export interface RenderWithAdaptersOptions<TAdapters> extends Omit<RenderOptions, 'wrapper'> {
5
+ /**
6
+ * The adapters context a shell would normally provide, paired with the value
7
+ * to publish on it.
8
+ */
9
+ adapters?: {
10
+ context: Context<TAdapters>;
11
+ value: TAdapters;
12
+ };
13
+ /** Defaults to a fresh in-memory store, so personalization never leaks. */
14
+ preferences?: UiPreferencesState;
15
+ }
16
+ export interface RenderWithAdaptersResult extends RenderResult {
17
+ /** The preferences store the tree rendered against. */
18
+ preferences: UiPreferencesState;
19
+ }
20
+ /**
21
+ * Renders a component with the providers a real page would mount around it.
22
+ *
23
+ * Components reach their adapters through context and their personalization
24
+ * through {@link UiPreferencesProvider}; rendering without those is testing a
25
+ * configuration that never ships.
26
+ */
27
+ export declare const renderWithAdapters: <TAdapters>(ui: ReactElement, { adapters, preferences, ...options }?: RenderWithAdaptersOptions<TAdapters>) => RenderWithAdaptersResult;
28
+ export * from '@testing-library/react';
29
+ export { default as userEvent } from '@testing-library/user-event';
30
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/react/render.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,OAAO,EAAqB,YAAY,EAAE,MAAM,OAAO,CAAC;AAItE,MAAM,WAAW,yBAAyB,CAAC,SAAS,CAAE,SAAQ,IAAI,CAChE,aAAa,EACb,SAAS,CACV;IACC;;;OAGG;IACH,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IAC7D,2EAA2E;IAC3E,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED,MAAM,WAAW,wBAAyB,SAAQ,YAAY;IAC5D,uDAAuD;IACvD,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,EAC1C,IAAI,YAAY,EAChB,wCAIG,yBAAyB,CAAC,SAAS,CAAM,KAC3C,wBAsBF,CAAC;AAEF,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { UiPreferencesProvider } from '@entifix/react-controls';
4
+ import { render } from '@testing-library/react';
5
+ import { makeInMemoryUiPreferencesState } from './ui-preferences.js';
6
+ /**
7
+ * Renders a component with the providers a real page would mount around it.
8
+ *
9
+ * Components reach their adapters through context and their personalization
10
+ * through {@link UiPreferencesProvider}; rendering without those is testing a
11
+ * configuration that never ships.
12
+ */ export const renderWithAdapters = (ui, _1 = void 0)=>{
13
+ let _ref = [
14
+ _1
15
+ ], [_ref1 = {}] = _ref, { adapters, preferences = makeInMemoryUiPreferencesState() } = _ref1, options = _object_without_properties_loose(_ref1, [
16
+ "adapters",
17
+ "preferences"
18
+ ]);
19
+ const Wrapper = ({ children })=>{
20
+ const withPreferences = /*#__PURE__*/ React.createElement(UiPreferencesProvider, {
21
+ store: preferences
22
+ }, children);
23
+ if (adapters === undefined) return withPreferences;
24
+ const { context: AdaptersContext, value } = adapters;
25
+ return /*#__PURE__*/ React.createElement(AdaptersContext.Provider, {
26
+ value: value
27
+ }, withPreferences);
28
+ };
29
+ return _extends({}, render(ui, _extends({
30
+ wrapper: Wrapper
31
+ }, options)), {
32
+ preferences
33
+ });
34
+ };
35
+ export * from '@testing-library/react';
36
+ export { default as userEvent } from '@testing-library/user-event';
37
+
38
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/render.tsx"],"sourcesContent":["import {\n UiPreferencesProvider,\n type UiPreferencesState,\n} from '@entifix/react-controls';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { Context, PropsWithChildren, ReactElement } from 'react';\n\nimport { makeInMemoryUiPreferencesState } from './ui-preferences.js';\n\nexport interface RenderWithAdaptersOptions<TAdapters> extends Omit<\n RenderOptions,\n 'wrapper'\n> {\n /**\n * The adapters context a shell would normally provide, paired with the value\n * to publish on it.\n */\n adapters?: { context: Context<TAdapters>; value: TAdapters };\n /** Defaults to a fresh in-memory store, so personalization never leaks. */\n preferences?: UiPreferencesState;\n}\n\nexport interface RenderWithAdaptersResult extends RenderResult {\n /** The preferences store the tree rendered against. */\n preferences: UiPreferencesState;\n}\n\n/**\n * Renders a component with the providers a real page would mount around it.\n *\n * Components reach their adapters through context and their personalization\n * through {@link UiPreferencesProvider}; rendering without those is testing a\n * configuration that never ships.\n */\nexport const renderWithAdapters = <TAdapters,>(\n ui: ReactElement,\n {\n adapters,\n preferences = makeInMemoryUiPreferencesState(),\n ...options\n }: RenderWithAdaptersOptions<TAdapters> = {},\n): RenderWithAdaptersResult => {\n const Wrapper = ({ children }: PropsWithChildren) => {\n const withPreferences = (\n <UiPreferencesProvider store={preferences}>\n {children}\n </UiPreferencesProvider>\n );\n\n if (adapters === undefined) return withPreferences;\n\n const { context: AdaptersContext, value } = adapters;\n return (\n <AdaptersContext.Provider value={value}>\n {withPreferences}\n </AdaptersContext.Provider>\n );\n };\n\n return {\n ...render(ui, { wrapper: Wrapper, ...options }),\n preferences,\n };\n};\n\nexport * from '@testing-library/react';\nexport { default as userEvent } from '@testing-library/user-event';\n"],"names":["UiPreferencesProvider","render","makeInMemoryUiPreferencesState","renderWithAdapters","ui","adapters","preferences","options","Wrapper","children","withPreferences","store","undefined","context","AdaptersContext","value","Provider","wrapper","default","userEvent"],"mappings":";;AAAA,SACEA,qBAAqB,QAEhB,0BAA0B;AACjC,SACEC,MAAM,QAGD,yBAAyB;AAGhC,SAASC,8BAA8B,QAAQ,sBAAsB;AAoBrE;;;;;;CAMC,GACD,OAAO,MAAMC,qBAAqB,CAChCC;;;gBAK0C,CAAC,WAJ3C,EACEC,QAAQ,EACRC,cAAcJ,gCAAgC,EAET,UADlCK;;;;IAGL,MAAMC,UAAU,CAAC,EAAEC,QAAQ,EAAqB;QAC9C,MAAMC,gCACJ,oBAACV;YAAsBW,OAAOL;WAC3BG;QAIL,IAAIJ,aAAaO,WAAW,OAAOF;QAEnC,MAAM,EAAEG,SAASC,eAAe,EAAEC,KAAK,EAAE,GAAGV;QAC5C,qBACE,oBAACS,gBAAgBE,QAAQ;YAACD,OAAOA;WAC9BL;IAGP;IAEA,OAAO,aACFT,OAAOG,IAAI;QAAEa,SAAST;OAAYD;QACrCD;;AAEJ,EAAE;AAEF,cAAc,yBAAyB;AACvC,SAASY,WAAWC,SAAS,QAAQ,8BAA8B"}
@@ -0,0 +1,17 @@
1
+ import type { UiPreferencesState } from '@entifix/react-controls';
2
+ export interface InMemoryUiPreferencesState extends UiPreferencesState {
3
+ /** Everything currently stored, keyed exactly as the controls wrote it. */
4
+ readonly entries: Readonly<Record<string, unknown>>;
5
+ /** Pre-populates the store, e.g. to render a table already personalized. */
6
+ seed(entries: Record<string, unknown>): void;
7
+ }
8
+ /**
9
+ * In-memory {@link UiPreferencesState}.
10
+ *
11
+ * The port is Effect-returning precisely so an asynchronous, server-backed
12
+ * store can replace `localStorage` without touching a component; this double
13
+ * exists so specs can assert on personalization without depending on jsdom
14
+ * storage behaviour at all.
15
+ */
16
+ export declare const makeInMemoryUiPreferencesState: (seed?: Record<string, unknown>) => InMemoryUiPreferencesState;
17
+ //# sourceMappingURL=ui-preferences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-preferences.d.ts","sourceRoot":"","sources":["../../src/react/ui-preferences.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGlE,MAAM,WAAW,0BAA2B,SAAQ,kBAAkB;IACpE,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,4EAA4E;IAC5E,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,GACzC,OAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,KACjC,0BAqBF,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { Effect } from 'effect';
3
+ /**
4
+ * In-memory {@link UiPreferencesState}.
5
+ *
6
+ * The port is Effect-returning precisely so an asynchronous, server-backed
7
+ * store can replace `localStorage` without touching a component; this double
8
+ * exists so specs can assert on personalization without depending on jsdom
9
+ * storage behaviour at all.
10
+ */ export const makeInMemoryUiPreferencesState = (seed = {})=>{
11
+ let entries = _extends({}, seed);
12
+ return {
13
+ read: (key)=>Effect.sync(()=>entries[key]),
14
+ write: (key, value)=>Effect.sync(()=>{
15
+ entries[key] = value;
16
+ }),
17
+ remove: (key)=>Effect.sync(()=>{
18
+ delete entries[key];
19
+ }),
20
+ get entries () {
21
+ return entries;
22
+ },
23
+ seed: (next)=>{
24
+ entries = _extends({}, next);
25
+ }
26
+ };
27
+ };
28
+
29
+ //# sourceMappingURL=ui-preferences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/ui-preferences.ts"],"sourcesContent":["import type { UiPreferencesState } from '@entifix/react-controls';\nimport { Effect } from 'effect';\n\nexport interface InMemoryUiPreferencesState extends UiPreferencesState {\n /** Everything currently stored, keyed exactly as the controls wrote it. */\n readonly entries: Readonly<Record<string, unknown>>;\n /** Pre-populates the store, e.g. to render a table already personalized. */\n seed(entries: Record<string, unknown>): void;\n}\n\n/**\n * In-memory {@link UiPreferencesState}.\n *\n * The port is Effect-returning precisely so an asynchronous, server-backed\n * store can replace `localStorage` without touching a component; this double\n * exists so specs can assert on personalization without depending on jsdom\n * storage behaviour at all.\n */\nexport const makeInMemoryUiPreferencesState = (\n seed: Record<string, unknown> = {},\n): InMemoryUiPreferencesState => {\n let entries: Record<string, unknown> = { ...seed };\n\n return {\n read: <TValue>(key: string) =>\n Effect.sync(() => entries[key] as TValue | undefined),\n write: <TValue>(key: string, value: TValue) =>\n Effect.sync(() => {\n entries[key] = value;\n }),\n remove: (key: string) =>\n Effect.sync(() => {\n delete entries[key];\n }),\n get entries() {\n return entries;\n },\n seed: next => {\n entries = { ...next };\n },\n };\n};\n"],"names":["Effect","makeInMemoryUiPreferencesState","seed","entries","read","key","sync","write","value","remove","next"],"mappings":";AACA,SAASA,MAAM,QAAQ,SAAS;AAShC;;;;;;;CAOC,GACD,OAAO,MAAMC,iCAAiC,CAC5CC,OAAgC,CAAC,CAAC;IAElC,IAAIC,UAAmC,aAAKD;IAE5C,OAAO;QACLE,MAAM,CAASC,MACbL,OAAOM,IAAI,CAAC,IAAMH,OAAO,CAACE,IAAI;QAChCE,OAAO,CAASF,KAAaG,QAC3BR,OAAOM,IAAI,CAAC;gBACVH,OAAO,CAACE,IAAI,GAAGG;YACjB;QACFC,QAAQ,CAACJ,MACPL,OAAOM,IAAI,CAAC;gBACV,OAAOH,OAAO,CAACE,IAAI;YACrB;QACF,IAAIF,WAAU;YACZ,OAAOA;QACT;QACAD,MAAMQ,CAAAA;YACJP,UAAU,aAAKO;QACjB;IACF;AACF,EAAE"}
package/package.json ADDED
@@ -0,0 +1,122 @@
1
+ {
2
+ "name": "@entifix/testing-unit",
3
+ "version": "0.1.0-0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/r10c-technologies/entifix.git",
8
+ "directory": "packages/ts/testing-unit"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist",
18
+ "!**/*.tsbuildinfo",
19
+ "LICENSE"
20
+ ],
21
+ "type": "module",
22
+ "exports": {
23
+ "./package.json": "./package.json",
24
+ ".": {
25
+ "@entifix/source": "./src/index.ts",
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./drivers": {
31
+ "@entifix/source": "./src/drivers/index.ts",
32
+ "types": "./dist/drivers/index.d.ts",
33
+ "import": "./dist/drivers/index.js",
34
+ "default": "./dist/drivers/index.js"
35
+ },
36
+ "./http": {
37
+ "@entifix/source": "./src/http/index.ts",
38
+ "types": "./dist/http/index.d.ts",
39
+ "import": "./dist/http/index.js",
40
+ "default": "./dist/http/index.js"
41
+ },
42
+ "./contracts": {
43
+ "@entifix/source": "./src/contracts/index.ts",
44
+ "types": "./dist/contracts/index.d.ts",
45
+ "import": "./dist/contracts/index.js",
46
+ "default": "./dist/contracts/index.js"
47
+ },
48
+ "./react": {
49
+ "@entifix/source": "./src/react/index.tsx",
50
+ "types": "./dist/react/index.d.ts",
51
+ "import": "./dist/react/index.js",
52
+ "default": "./dist/react/index.js"
53
+ }
54
+ },
55
+ "nx": {
56
+ "tags": [
57
+ "layer:entifix",
58
+ "scope:shared",
59
+ "type:testing",
60
+ "tier:5"
61
+ ],
62
+ "sourceRoot": "packages/ts/testing-unit/src",
63
+ "targets": {
64
+ "build": {
65
+ "executor": "@nx/js:swc",
66
+ "outputs": [
67
+ "{options.outputPath}"
68
+ ],
69
+ "options": {
70
+ "outputPath": "packages/ts/testing-unit/dist",
71
+ "main": "packages/ts/testing-unit/src/index.ts",
72
+ "tsConfig": "packages/ts/testing-unit/tsconfig.lib.json",
73
+ "skipTypeCheck": true,
74
+ "stripLeadingPaths": true
75
+ }
76
+ }
77
+ }
78
+ },
79
+ "dependencies": {
80
+ "@effect/sql": "0.52.1",
81
+ "@swc/helpers": "0.5.23",
82
+ "@entifix/business": "0.1.0-0",
83
+ "@entifix/core": "0.1.0-0",
84
+ "@entifix/transactions": "0.1.0-0"
85
+ },
86
+ "peerDependencies": {
87
+ "@effect/vitest": ">=0.30 <1",
88
+ "@testing-library/react": "*",
89
+ "effect": ">=3.22 <4",
90
+ "msw": "*",
91
+ "react": "*",
92
+ "vitest": "*",
93
+ "@entifix/react-controls": "0.1.0-0"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "@effect/vitest": {
97
+ "optional": true
98
+ },
99
+ "@entifix/react-controls": {
100
+ "optional": true
101
+ },
102
+ "@testing-library/react": {
103
+ "optional": true
104
+ },
105
+ "@testing-library/user-event": {
106
+ "optional": true
107
+ },
108
+ "msw": {
109
+ "optional": true
110
+ },
111
+ "react": {
112
+ "optional": true
113
+ },
114
+ "vitest": {
115
+ "optional": true
116
+ }
117
+ },
118
+ "devDependencies": {
119
+ "@effect/vitest": "^0.30.0",
120
+ "effect": "3.22.1"
121
+ }
122
+ }