@mittwald/react-ghostmaker 1.0.4 → 1.2.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/README.md CHANGED
@@ -455,15 +455,22 @@ function BlogPage() {
455
455
  }
456
456
  ```
457
457
 
458
+
458
459
  ## 🏗️ Working with Domain Models
459
460
 
460
461
  ### Model Identification
461
462
 
462
- Register model identifiers for better caching:
463
+ #### Using the `GhostMakerModel` Decorator (Recommended)
464
+
465
+ You can now use the `GhostMakerModel` decorator to define model names and IDs for automatic query key generation. This is the recommended and most convenient way to ensure stable and meaningful cache keys for your models.
463
466
 
464
467
  ```tsx
465
- import { registerModelIdentifier } from "@mittwald/react-ghostmaker";
468
+ import { GhostMakerModel } from "@mittwald/react-ghostmaker";
466
469
 
470
+ @GhostMakerModel({
471
+ name: "User",
472
+ getId: (user) => user.id,
473
+ })
467
474
  class User {
468
475
  constructor(
469
476
  public id: string,
@@ -471,14 +478,32 @@ class User {
471
478
  ) {}
472
479
  }
473
480
 
481
+ @GhostMakerModel({
482
+ name: "Blog",
483
+ getId: (blog) => blog.id,
484
+ })
474
485
  class Blog {
475
486
  constructor(
476
487
  public id: string,
477
488
  public title: string,
478
489
  ) {}
479
490
  }
491
+ ```
492
+
493
+ **Advantages:**
494
+
495
+ - Declarative, directly on the model class
496
+ - No need for central registration
497
+ - Name and ID can be set explicitly (for readable query keys)
498
+ - Works for multiple models and inheritance
499
+
500
+ #### Alternative: `registerModelIdentifier` (deprecated)
501
+
502
+ The previous `registerModelIdentifier` function is still available but marked as deprecated. It can be used to centrally register IDs for models:
503
+
504
+ ```tsx
505
+ import { registerModelIdentifier } from "@mittwald/react-ghostmaker";
480
506
 
481
- // Register identifiers for automatic cache key generation
482
507
  registerModelIdentifier((model) => {
483
508
  if (model instanceof User) return `user-${model.id}`;
484
509
  if (model instanceof Blog) return `blog-${model.id}`;
@@ -486,6 +511,8 @@ registerModelIdentifier((model) => {
486
511
  });
487
512
  ```
488
513
 
514
+ > **Note:** Prefer the `GhostMakerModel` decorator for new projects.
515
+
489
516
  ## 🎭 Flexible Component Props with MaybeReactGhost
490
517
 
491
518
  The `MaybeReactGhost` pattern allows your components to accept both ghost
@@ -2,3 +2,8 @@ import { createCascade } from "context";
2
2
  export const ghostFnContext = createCascade();
3
3
  const useGhostFnContext = () => ghostFnContext.use();
4
4
  export const getQueryContext = () => useGhostFnContext()?.query;
5
+ export function forwardQueryContext(fn) {
6
+ const queryContext = getQueryContext();
7
+ ghostFnContext.bind({ query: queryContext }, fn);
8
+ return fn();
9
+ }
package/dist/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { makeGhost } from "./makeGhost";
2
- export { getQueryContext } from "./context";
2
+ export { getQueryContext, forwardQueryContext } from "./context";
3
3
  export { invalidateGhosts } from "./invalidate";
4
- export { registerModelIdentifier } from "./modelIdentifier";
5
4
  export * from "./maybeGhost";
5
+ export { GhostMakerModel } from "./metaData";
6
+ export { getModelId, getModelName } from "./queries";
@@ -0,0 +1,9 @@
1
+ const store = new Map();
2
+ export const getMetaData = (something) => {
3
+ return store.get(something);
4
+ };
5
+ export function GhostMakerModel(options) {
6
+ return function (target) {
7
+ store.set(target, options);
8
+ };
9
+ }
@@ -1,4 +1,5 @@
1
1
  export const modelIdentifiers = [];
2
+ /** @deprecated Use GhostMakerModel (decorator) instead */
2
3
  export const registerModelIdentifier = (identifier) => {
3
4
  modelIdentifiers.push(identifier);
4
5
  };
@@ -1,18 +1,61 @@
1
1
  import is from "@sindresorhus/is";
2
- import { modelIdentifiers } from "./modelIdentifier";
3
2
  import { hashObject } from "./hash";
4
- const getTargetQueryKey = (something) => {
5
- if (is.primitive(something)) {
6
- return String(something);
7
- }
3
+ import { getMetaData } from "./metaData";
4
+ import { modelIdentifiers } from "./modelIdentifier";
5
+ export const getModelName = (something) => {
6
+ const isObject = is.object(something);
7
+ const getModelNameRecursive = (
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
9
+ klass) => {
10
+ if (!klass) {
11
+ return;
12
+ }
13
+ return (getMetaData(klass)?.name ??
14
+ getModelNameRecursive(Object.getPrototypeOf(klass)));
15
+ };
16
+ const klass = isObject ? something.constructor : undefined;
17
+ return getModelNameRecursive(klass);
18
+ };
19
+ export const getModelId = (something) => {
8
20
  const isObject = is.object(something);
21
+ const getIdRecursive = (
22
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
23
+ klass) => {
24
+ if (!klass) {
25
+ return;
26
+ }
27
+ const getId = getMetaData(klass)?.getId;
28
+ if (getId) {
29
+ return getId(something);
30
+ }
31
+ return getIdRecursive(Object.getPrototypeOf(klass));
32
+ };
33
+ const klass = isObject ? something.constructor : undefined;
34
+ return getIdRecursive(klass);
35
+ };
36
+ const getObjectName = (something) => {
37
+ const modelName = getModelName(something);
38
+ if (modelName) {
39
+ return modelName;
40
+ }
9
41
  const isClass = is.class(something);
10
42
  const isFunction = is.function(something);
11
- const objectName = isFunction
43
+ const isObject = is.object(something);
44
+ return isFunction
12
45
  ? something.name
13
46
  : isClass || isObject
14
47
  ? something.constructor.name
15
48
  : "unknown";
49
+ };
50
+ const getTargetQueryKey = (something) => {
51
+ if (is.primitive(something)) {
52
+ return String(something);
53
+ }
54
+ const objectName = getObjectName(something);
55
+ const id = getModelId(something);
56
+ if (id) {
57
+ return `${objectName}@${id}`;
58
+ }
16
59
  const thisModelIdentifiers = modelIdentifiers
17
60
  .map((fn) => fn(something))
18
61
  .filter(is.string)
@@ -26,7 +69,7 @@ const getArgKey = (arg) => {
26
69
  if (is.primitive(arg)) {
27
70
  return String(arg);
28
71
  }
29
- return hashObject(arg).toString();
72
+ return `${getObjectName(arg)}(hash:${hashObject(arg)})`;
30
73
  };
31
74
  export const queries = {
32
75
  ghostmaker: () => ["react-ghostmaker"],
@@ -1,5 +1,40 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
1
35
  import { beforeEach, vitest } from "vitest";
2
- import { getQueryContext, makeGhost, registerModelIdentifier } from ".";
36
+ import { getQueryContext, makeGhost } from ".";
37
+ import { GhostMakerModel } from "./metaData";
3
38
  const sleep = () => new Promise((resolve) => setTimeout(resolve, 200));
4
39
  export const advanceSleepTimer = async (times = 1) => {
5
40
  for (let i = 0; i < times; i++) {
@@ -29,71 +64,145 @@ beforeEach(() => {
29
64
  getStatus: vitest.fn(),
30
65
  };
31
66
  });
32
- export class CustomerDetailed {
33
- id;
34
- name;
35
- constructor(id, name) {
36
- this.id = id;
37
- this.name = name;
38
- }
39
- getName() {
40
- customerMocks.getName();
41
- return this.name;
42
- }
43
- }
44
- export class Customer {
45
- id;
46
- constructor(id) {
47
- this.id = id;
48
- }
49
- static ofId(id) {
50
- return new Customer(id);
51
- }
52
- async getDetailed() {
53
- return Customer.get(this.id);
54
- }
55
- static async get(id) {
56
- await sleep();
57
- return customerMocks.getDetailed(id);
58
- }
59
- }
60
- export class Project {
61
- id;
62
- constructor(id) {
63
- this.id = id;
64
- }
65
- static ofId(id) {
66
- return new Project(id);
67
- }
68
- async getDetailed() {
69
- await sleep();
70
- return projectMocks.getDetailed(this.id);
71
- }
72
- async findDetailed() {
73
- await sleep();
74
- return projectMocks.findDetailed(this.id);
75
- }
76
- }
77
- export class ProjectDetailed {
78
- id;
79
- name;
80
- customer;
81
- constructor(id, name, customerId) {
82
- this.id = id;
83
- this.name = name;
84
- this.customer = new Customer(customerId);
85
- }
86
- getName() {
87
- projectMocks.getName();
88
- return this.name;
89
- }
90
- async getStatus() {
91
- projectMocks.getStatus();
92
- await sleep();
93
- return "active";
94
- }
95
- }
67
+ let CustomerDetailed = (() => {
68
+ let _classDecorators = [GhostMakerModel({
69
+ getId: (customer) => customer.id,
70
+ name: "CustomerDetailed",
71
+ })];
72
+ let _classDescriptor;
73
+ let _classExtraInitializers = [];
74
+ let _classThis;
75
+ var CustomerDetailed = class {
76
+ static { _classThis = this; }
77
+ static {
78
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
79
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
80
+ CustomerDetailed = _classThis = _classDescriptor.value;
81
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
82
+ __runInitializers(_classThis, _classExtraInitializers);
83
+ }
84
+ id;
85
+ name;
86
+ constructor(id, name) {
87
+ this.id = id;
88
+ this.name = name;
89
+ }
90
+ getName() {
91
+ customerMocks.getName();
92
+ return this.name;
93
+ }
94
+ };
95
+ return CustomerDetailed = _classThis;
96
+ })();
97
+ export { CustomerDetailed };
98
+ let Customer = (() => {
99
+ let _classDecorators = [GhostMakerModel({
100
+ getId: (customer) => customer.id,
101
+ name: "Customer",
102
+ })];
103
+ let _classDescriptor;
104
+ let _classExtraInitializers = [];
105
+ let _classThis;
106
+ var Customer = class {
107
+ static { _classThis = this; }
108
+ static {
109
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
110
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
111
+ Customer = _classThis = _classDescriptor.value;
112
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
113
+ __runInitializers(_classThis, _classExtraInitializers);
114
+ }
115
+ id;
116
+ constructor(id) {
117
+ this.id = id;
118
+ }
119
+ static ofId(id) {
120
+ return new Customer(id);
121
+ }
122
+ async getDetailed() {
123
+ return Customer.get(this.id);
124
+ }
125
+ static async get(id) {
126
+ await sleep();
127
+ return customerMocks.getDetailed(id);
128
+ }
129
+ };
130
+ return Customer = _classThis;
131
+ })();
132
+ export { Customer };
133
+ let Project = (() => {
134
+ let _classDecorators = [GhostMakerModel({
135
+ getId: (project) => project.id,
136
+ name: "Project",
137
+ })];
138
+ let _classDescriptor;
139
+ let _classExtraInitializers = [];
140
+ let _classThis;
141
+ var Project = class {
142
+ static { _classThis = this; }
143
+ static {
144
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
145
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
146
+ Project = _classThis = _classDescriptor.value;
147
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
148
+ __runInitializers(_classThis, _classExtraInitializers);
149
+ }
150
+ id;
151
+ constructor(id) {
152
+ this.id = id;
153
+ }
154
+ static ofId(id) {
155
+ return new Project(id);
156
+ }
157
+ async getDetailed() {
158
+ await sleep();
159
+ return projectMocks.getDetailed(this.id);
160
+ }
161
+ async findDetailed() {
162
+ await sleep();
163
+ return projectMocks.findDetailed(this.id);
164
+ }
165
+ };
166
+ return Project = _classThis;
167
+ })();
168
+ export { Project };
169
+ let ProjectDetailed = (() => {
170
+ let _classDecorators = [GhostMakerModel({
171
+ getId: (project) => project.id,
172
+ name: "ProjectDetailed",
173
+ })];
174
+ let _classDescriptor;
175
+ let _classExtraInitializers = [];
176
+ let _classThis;
177
+ var ProjectDetailed = class {
178
+ static { _classThis = this; }
179
+ static {
180
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
181
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
182
+ ProjectDetailed = _classThis = _classDescriptor.value;
183
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
184
+ __runInitializers(_classThis, _classExtraInitializers);
185
+ }
186
+ id;
187
+ name;
188
+ customer;
189
+ constructor(id, name, customerId) {
190
+ this.id = id;
191
+ this.name = name;
192
+ this.customer = new Customer(customerId);
193
+ }
194
+ getName() {
195
+ projectMocks.getName();
196
+ return this.name;
197
+ }
198
+ async getStatus() {
199
+ projectMocks.getStatus();
200
+ await sleep();
201
+ return "active";
202
+ }
203
+ };
204
+ return ProjectDetailed = _classThis;
205
+ })();
206
+ export { ProjectDetailed };
96
207
  export const ProjectGhost = makeGhost(Project);
97
208
  export const CustomerGhost = makeGhost(Customer);
98
- registerModelIdentifier((model) => model instanceof Project ? model.id : undefined);
99
- registerModelIdentifier((model) => model instanceof Customer ? model.id : undefined);
@@ -12,3 +12,4 @@ export declare const getQueryContext: () => {
12
12
  pageParam?: unknown;
13
13
  direction?: unknown;
14
14
  } | undefined;
15
+ export declare function forwardQueryContext<T>(fn: () => T): T;
@@ -1,6 +1,7 @@
1
1
  export { makeGhost } from "./makeGhost";
2
2
  export { type ReactGhost, UseGhostReturn } from "./types";
3
- export { getQueryContext } from "./context";
3
+ export { getQueryContext, forwardQueryContext } from "./context";
4
4
  export { invalidateGhosts } from "./invalidate";
5
- export { registerModelIdentifier } from "./modelIdentifier";
6
5
  export * from "./maybeGhost";
6
+ export { GhostMakerModel } from "./metaData";
7
+ export { getModelId, getModelName } from "./queries";
@@ -0,0 +1,8 @@
1
+ import type { Class } from "type-fest";
2
+ interface GhostMakerModelOptions<T extends Class<unknown>> {
3
+ getId?: (instance: InstanceType<T>) => string;
4
+ name?: string;
5
+ }
6
+ export declare const getMetaData: (something: unknown) => GhostMakerModelOptions<Class<unknown>> | undefined;
7
+ export declare function GhostMakerModel<T extends Class<unknown>>(options: GhostMakerModelOptions<T>): (target: T) => void;
8
+ export {};
@@ -1,3 +1,4 @@
1
1
  import type { ModelIdentifier } from "./types";
2
2
  export declare const modelIdentifiers: ModelIdentifier[];
3
+ /** @deprecated Use GhostMakerModel (decorator) instead */
3
4
  export declare const registerModelIdentifier: (identifier: ModelIdentifier) => void;
@@ -1,4 +1,6 @@
1
1
  import type { GhostChain, GhostChainItem, QueryKey } from "./types";
2
+ export declare const getModelName: (something: unknown) => string | undefined;
3
+ export declare const getModelId: (something: unknown) => string | undefined;
2
4
  export declare const queries: {
3
5
  ghostmaker: () => string[];
4
6
  target: (target: unknown) => string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/react-ghostmaker",
3
- "version": "1.0.4",
3
+ "version": "1.2.0",
4
4
  "author": "Mittwald CM Service GmbH & Co. KG <opensource@mittwald.de>",
5
5
  "contributors": [
6
6
  "Marco Falkenberg <m.falkenberg@mittwald.de>"