@mittwald/react-ghostmaker 1.1.0 → 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 +30 -3
- package/dist/esm/context.js +5 -0
- package/dist/esm/index.js +3 -2
- package/dist/esm/metaData.js +9 -0
- package/dist/esm/modelIdentifier.js +1 -0
- package/dist/esm/queries.js +42 -2
- package/dist/esm/testMocks.js +176 -67
- package/dist/types/context.d.ts +1 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/metaData.d.ts +8 -0
- package/dist/types/modelIdentifier.d.ts +1 -0
- package/dist/types/queries.d.ts +2 -0
- package/package.json +1 -1
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
|
-
|
|
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 {
|
|
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
|
package/dist/esm/context.js
CHANGED
|
@@ -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";
|
package/dist/esm/queries.js
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
import is from "@sindresorhus/is";
|
|
2
|
-
import { modelIdentifiers } from "./modelIdentifier";
|
|
3
2
|
import { hashObject } from "./hash";
|
|
4
|
-
|
|
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) => {
|
|
5
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
|
+
}
|
|
6
41
|
const isClass = is.class(something);
|
|
7
42
|
const isFunction = is.function(something);
|
|
43
|
+
const isObject = is.object(something);
|
|
8
44
|
return isFunction
|
|
9
45
|
? something.name
|
|
10
46
|
: isClass || isObject
|
|
@@ -16,6 +52,10 @@ const getTargetQueryKey = (something) => {
|
|
|
16
52
|
return String(something);
|
|
17
53
|
}
|
|
18
54
|
const objectName = getObjectName(something);
|
|
55
|
+
const id = getModelId(something);
|
|
56
|
+
if (id) {
|
|
57
|
+
return `${objectName}@${id}`;
|
|
58
|
+
}
|
|
19
59
|
const thisModelIdentifiers = modelIdentifiers
|
|
20
60
|
.map((fn) => fn(something))
|
|
21
61
|
.filter(is.string)
|
package/dist/esm/testMocks.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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);
|
package/dist/types/context.d.ts
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/types/queries.d.ts
CHANGED
|
@@ -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[];
|