@jay-framework/fullstack-component 0.9.0 → 0.11.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 +371 -2
- package/dist/index.d.ts +469 -16
- package/dist/index.js +381 -11
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -4,34 +4,52 @@ var __publicField = (obj, key, value) => {
|
|
|
4
4
|
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
5
|
return value;
|
|
6
6
|
};
|
|
7
|
-
function serverError5xx(status) {
|
|
7
|
+
function serverError5xx(status, message, details) {
|
|
8
8
|
return {
|
|
9
9
|
kind: "ServerError",
|
|
10
|
-
status
|
|
10
|
+
status,
|
|
11
|
+
message,
|
|
12
|
+
details
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
|
-
function clientError4xx(status) {
|
|
15
|
+
function clientError4xx(status, message, details) {
|
|
14
16
|
return {
|
|
15
17
|
kind: "ClientError",
|
|
16
|
-
status
|
|
18
|
+
status,
|
|
19
|
+
message,
|
|
20
|
+
details
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
|
-
function notFound() {
|
|
20
|
-
return clientError4xx(404);
|
|
23
|
+
function notFound(message, details) {
|
|
24
|
+
return clientError4xx(404, message, details);
|
|
25
|
+
}
|
|
26
|
+
function badRequest(message, details) {
|
|
27
|
+
return clientError4xx(400, message, details);
|
|
21
28
|
}
|
|
22
|
-
function
|
|
29
|
+
function unauthorized(message, details) {
|
|
30
|
+
return clientError4xx(401, message, details);
|
|
31
|
+
}
|
|
32
|
+
function forbidden(message, details) {
|
|
33
|
+
return clientError4xx(403, message, details);
|
|
34
|
+
}
|
|
35
|
+
function redirect3xx(status, location, message) {
|
|
23
36
|
return {
|
|
24
|
-
kind: "
|
|
37
|
+
kind: "Redirect",
|
|
25
38
|
status,
|
|
26
|
-
location
|
|
39
|
+
location,
|
|
40
|
+
message
|
|
27
41
|
};
|
|
28
42
|
}
|
|
43
|
+
function phaseOutput(rendered, carryForward) {
|
|
44
|
+
return { kind: "PhaseOutput", rendered, carryForward };
|
|
45
|
+
}
|
|
29
46
|
function partialRender(rendered, carryForward) {
|
|
30
|
-
return
|
|
47
|
+
return phaseOutput(rendered, carryForward);
|
|
31
48
|
}
|
|
32
49
|
function createJayService(name) {
|
|
33
50
|
return Symbol(name);
|
|
34
51
|
}
|
|
52
|
+
const DYNAMIC_CONTRACT_SERVICE = createJayService("DynamicContract");
|
|
35
53
|
class BuilderImplementation {
|
|
36
54
|
constructor() {
|
|
37
55
|
__publicField(this, "services", []);
|
|
@@ -72,12 +90,364 @@ class BuilderImplementation {
|
|
|
72
90
|
function makeJayStackComponent() {
|
|
73
91
|
return new BuilderImplementation();
|
|
74
92
|
}
|
|
93
|
+
class ContractGeneratorBuilderImpl {
|
|
94
|
+
constructor(serviceMarkers = []) {
|
|
95
|
+
__publicField(this, "serviceMarkers");
|
|
96
|
+
this.serviceMarkers = serviceMarkers;
|
|
97
|
+
}
|
|
98
|
+
withServices(...serviceMarkers) {
|
|
99
|
+
return new ContractGeneratorBuilderImpl(serviceMarkers);
|
|
100
|
+
}
|
|
101
|
+
generateWith(fn) {
|
|
102
|
+
return {
|
|
103
|
+
services: this.serviceMarkers,
|
|
104
|
+
generate: fn
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function makeContractGenerator() {
|
|
109
|
+
return new ContractGeneratorBuilderImpl();
|
|
110
|
+
}
|
|
111
|
+
function isRenderPipeline(value) {
|
|
112
|
+
return value instanceof RenderPipeline;
|
|
113
|
+
}
|
|
114
|
+
function isErrorOutcome(value) {
|
|
115
|
+
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "ServerError" || value.kind === "ClientError" || value.kind === "Redirect");
|
|
116
|
+
}
|
|
117
|
+
class RenderPipeline {
|
|
118
|
+
constructor(_value, _isSuccess) {
|
|
119
|
+
this._value = _value;
|
|
120
|
+
this._isSuccess = _isSuccess;
|
|
121
|
+
}
|
|
122
|
+
// =========================================================================
|
|
123
|
+
// Static Factory
|
|
124
|
+
// =========================================================================
|
|
125
|
+
/**
|
|
126
|
+
* Create a typed pipeline factory with target output types declared upfront.
|
|
127
|
+
* TypeScript validates that .toPhaseOutput() produces these types.
|
|
128
|
+
*/
|
|
129
|
+
static for() {
|
|
130
|
+
return {
|
|
131
|
+
ok(value) {
|
|
132
|
+
return new RenderPipeline(value, true);
|
|
133
|
+
},
|
|
134
|
+
try(fn) {
|
|
135
|
+
try {
|
|
136
|
+
const result = fn();
|
|
137
|
+
if (result instanceof Promise) {
|
|
138
|
+
const wrappedPromise = result.catch((error) => {
|
|
139
|
+
throw { __pipelineError: true, error };
|
|
140
|
+
});
|
|
141
|
+
return new RenderPipeline(
|
|
142
|
+
wrappedPromise,
|
|
143
|
+
true
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return new RenderPipeline(result, true);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return new RenderPipeline(
|
|
149
|
+
{ __caughtError: error },
|
|
150
|
+
true
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
from(outcome) {
|
|
155
|
+
if (outcome.kind === "PhaseOutput") {
|
|
156
|
+
return new RenderPipeline(outcome.rendered, true);
|
|
157
|
+
}
|
|
158
|
+
return new RenderPipeline(outcome, false);
|
|
159
|
+
},
|
|
160
|
+
notFound(message, details) {
|
|
161
|
+
return new RenderPipeline(
|
|
162
|
+
notFound(message, details),
|
|
163
|
+
false
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
badRequest(message, details) {
|
|
167
|
+
return new RenderPipeline(
|
|
168
|
+
badRequest(message, details),
|
|
169
|
+
false
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
unauthorized(message, details) {
|
|
173
|
+
return new RenderPipeline(
|
|
174
|
+
unauthorized(message, details),
|
|
175
|
+
false
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
forbidden(message, details) {
|
|
179
|
+
return new RenderPipeline(
|
|
180
|
+
forbidden(message, details),
|
|
181
|
+
false
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
serverError(status, message, details) {
|
|
185
|
+
return new RenderPipeline(
|
|
186
|
+
serverError5xx(status, message, details),
|
|
187
|
+
false
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
clientError(status, message, details) {
|
|
191
|
+
return new RenderPipeline(
|
|
192
|
+
clientError4xx(status, message, details),
|
|
193
|
+
false
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
redirect(status, location) {
|
|
197
|
+
return new RenderPipeline(
|
|
198
|
+
redirect3xx(status, location),
|
|
199
|
+
false
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// =========================================================================
|
|
205
|
+
// Transformation Methods
|
|
206
|
+
// =========================================================================
|
|
207
|
+
/**
|
|
208
|
+
* Transform the working value. Always returns RenderPipeline (sync).
|
|
209
|
+
*
|
|
210
|
+
* The mapping function can return:
|
|
211
|
+
* - U: Plain value
|
|
212
|
+
* - Promise<U>: Async value (resolved at toPhaseOutput)
|
|
213
|
+
* - RenderPipeline<U>: For conditional errors/branching
|
|
214
|
+
*
|
|
215
|
+
* Errors pass through unchanged.
|
|
216
|
+
*/
|
|
217
|
+
map(fn) {
|
|
218
|
+
if (!this._isSuccess) {
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
if (this._value instanceof Promise) {
|
|
222
|
+
const chainedPromise = this._value.then((resolvedValue) => {
|
|
223
|
+
if (isRenderPipeline(resolvedValue)) {
|
|
224
|
+
return resolvedValue.map(fn)._value;
|
|
225
|
+
}
|
|
226
|
+
if (isErrorOutcome(resolvedValue)) {
|
|
227
|
+
return resolvedValue;
|
|
228
|
+
}
|
|
229
|
+
return fn(resolvedValue);
|
|
230
|
+
});
|
|
231
|
+
return new RenderPipeline(
|
|
232
|
+
chainedPromise,
|
|
233
|
+
true
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (isErrorOutcome(this._value)) {
|
|
237
|
+
return new RenderPipeline(this._value, false);
|
|
238
|
+
}
|
|
239
|
+
if (isRenderPipeline(this._value)) {
|
|
240
|
+
return this._value.map(fn);
|
|
241
|
+
}
|
|
242
|
+
const result = fn(this._value);
|
|
243
|
+
if (isRenderPipeline(result)) {
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
return new RenderPipeline(result, true);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Handle errors, potentially recovering to a success.
|
|
250
|
+
* The function receives the caught Error and can return a new pipeline.
|
|
251
|
+
*/
|
|
252
|
+
recover(fn) {
|
|
253
|
+
if (!this._isSuccess && isErrorOutcome(this._value)) {
|
|
254
|
+
const error = new Error(
|
|
255
|
+
this._value.message || `${this._value.kind}: ${this._value.status}`
|
|
256
|
+
);
|
|
257
|
+
error.outcome = this._value;
|
|
258
|
+
return fn(error);
|
|
259
|
+
}
|
|
260
|
+
if (this._value instanceof Promise) {
|
|
261
|
+
const recoveredPromise = this._value.then((resolved) => {
|
|
262
|
+
if (isRenderPipeline(resolved)) {
|
|
263
|
+
return resolved.recover(fn)._value;
|
|
264
|
+
}
|
|
265
|
+
if (isErrorOutcome(resolved)) {
|
|
266
|
+
const error = new Error(
|
|
267
|
+
resolved.message || `${resolved.kind}: ${resolved.status}`
|
|
268
|
+
);
|
|
269
|
+
error.outcome = resolved;
|
|
270
|
+
return fn(error)._value;
|
|
271
|
+
}
|
|
272
|
+
return resolved;
|
|
273
|
+
}).catch((caught) => {
|
|
274
|
+
let actualError = caught;
|
|
275
|
+
if (typeof caught === "object" && caught !== null && "__pipelineError" in caught) {
|
|
276
|
+
actualError = caught.error;
|
|
277
|
+
}
|
|
278
|
+
const error = actualError instanceof Error ? actualError : new Error(String(actualError));
|
|
279
|
+
return fn(error)._value;
|
|
280
|
+
});
|
|
281
|
+
return new RenderPipeline(
|
|
282
|
+
recoveredPromise,
|
|
283
|
+
true
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (typeof this._value === "object" && this._value !== null && "__caughtError" in this._value) {
|
|
287
|
+
const caught = this._value.__caughtError;
|
|
288
|
+
const error = caught instanceof Error ? caught : new Error(String(caught));
|
|
289
|
+
return fn(error);
|
|
290
|
+
}
|
|
291
|
+
return this;
|
|
292
|
+
}
|
|
293
|
+
// =========================================================================
|
|
294
|
+
// Terminal Methods
|
|
295
|
+
// =========================================================================
|
|
296
|
+
/**
|
|
297
|
+
* Convert to final PhaseOutput. This is the ONLY async method.
|
|
298
|
+
* Resolves all pending promises and applies the final mapping.
|
|
299
|
+
*/
|
|
300
|
+
async toPhaseOutput(fn) {
|
|
301
|
+
let resolvedValue;
|
|
302
|
+
if (this._value instanceof Promise) {
|
|
303
|
+
try {
|
|
304
|
+
resolvedValue = await this._value;
|
|
305
|
+
} catch (caught) {
|
|
306
|
+
let actualError = caught;
|
|
307
|
+
if (typeof caught === "object" && caught !== null && "__pipelineError" in caught) {
|
|
308
|
+
actualError = caught.error;
|
|
309
|
+
}
|
|
310
|
+
const message = actualError instanceof Error ? actualError.message : String(actualError);
|
|
311
|
+
return serverError5xx(500, message);
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
resolvedValue = this._value;
|
|
315
|
+
}
|
|
316
|
+
if (typeof resolvedValue === "object" && resolvedValue !== null && "__caughtError" in resolvedValue) {
|
|
317
|
+
const caught = resolvedValue.__caughtError;
|
|
318
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
319
|
+
return serverError5xx(500, message);
|
|
320
|
+
}
|
|
321
|
+
if (isRenderPipeline(resolvedValue)) {
|
|
322
|
+
return resolvedValue.toPhaseOutput(fn);
|
|
323
|
+
}
|
|
324
|
+
if (isErrorOutcome(resolvedValue)) {
|
|
325
|
+
return resolvedValue;
|
|
326
|
+
}
|
|
327
|
+
const { viewState, carryForward } = fn(resolvedValue);
|
|
328
|
+
return phaseOutput(viewState, carryForward);
|
|
329
|
+
}
|
|
330
|
+
// =========================================================================
|
|
331
|
+
// Utility Methods
|
|
332
|
+
// =========================================================================
|
|
333
|
+
/** Check if this pipeline is in a success state */
|
|
334
|
+
isOk() {
|
|
335
|
+
return this._isSuccess;
|
|
336
|
+
}
|
|
337
|
+
/** Check if this pipeline is in an error state */
|
|
338
|
+
isError() {
|
|
339
|
+
return !this._isSuccess;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
class ActionError extends Error {
|
|
343
|
+
constructor(code, message) {
|
|
344
|
+
super(message);
|
|
345
|
+
__publicField(this, "name", "ActionError");
|
|
346
|
+
this.code = code;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
class JayActionBuilderImpl {
|
|
350
|
+
constructor(_actionName, defaultMethod) {
|
|
351
|
+
__publicField(this, "_services", []);
|
|
352
|
+
__publicField(this, "_method");
|
|
353
|
+
__publicField(this, "_cacheOptions");
|
|
354
|
+
this._actionName = _actionName;
|
|
355
|
+
this._method = defaultMethod;
|
|
356
|
+
}
|
|
357
|
+
withServices(...services) {
|
|
358
|
+
this._services = services;
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
withMethod(method) {
|
|
362
|
+
this._method = method;
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
withCaching(options) {
|
|
366
|
+
this._cacheOptions = options ?? { maxAge: 60 };
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
withHandler(handler) {
|
|
370
|
+
const actionName = this._actionName;
|
|
371
|
+
const method = this._method;
|
|
372
|
+
const cacheOptions = this._cacheOptions;
|
|
373
|
+
const services = this._services;
|
|
374
|
+
const action = Object.assign(
|
|
375
|
+
(input) => handler(input, ...[]),
|
|
376
|
+
{
|
|
377
|
+
actionName,
|
|
378
|
+
method,
|
|
379
|
+
cacheOptions,
|
|
380
|
+
services,
|
|
381
|
+
handler,
|
|
382
|
+
_brand: "JayAction"
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
return action;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function makeJayAction(name) {
|
|
389
|
+
return new JayActionBuilderImpl(name, "POST");
|
|
390
|
+
}
|
|
391
|
+
function makeJayQuery(name) {
|
|
392
|
+
return new JayActionBuilderImpl(name, "GET");
|
|
393
|
+
}
|
|
394
|
+
function isJayAction(value) {
|
|
395
|
+
return typeof value === "function" && value._brand === "JayAction" && typeof value.actionName === "string";
|
|
396
|
+
}
|
|
397
|
+
function makeJayInit(key) {
|
|
398
|
+
const resolvedKey = key ?? "__JAY_INIT_KEY__";
|
|
399
|
+
return {
|
|
400
|
+
withServer(callback) {
|
|
401
|
+
const serverOnlyInit = {
|
|
402
|
+
__brand: "JayInit",
|
|
403
|
+
key: resolvedKey,
|
|
404
|
+
_serverInit: callback
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
// JayInit properties (allows using as server-only init)
|
|
408
|
+
...serverOnlyInit,
|
|
409
|
+
// Builder method to add client init
|
|
410
|
+
withClient(clientCallback) {
|
|
411
|
+
return {
|
|
412
|
+
__brand: "JayInit",
|
|
413
|
+
key: resolvedKey,
|
|
414
|
+
_serverInit: callback,
|
|
415
|
+
_clientInit: clientCallback
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
},
|
|
420
|
+
withClient(callback) {
|
|
421
|
+
return {
|
|
422
|
+
__brand: "JayInit",
|
|
423
|
+
key: resolvedKey,
|
|
424
|
+
_clientInit: callback
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function isJayInit(obj) {
|
|
430
|
+
return typeof obj === "object" && obj !== null && "__brand" in obj && obj.__brand === "JayInit";
|
|
431
|
+
}
|
|
75
432
|
export {
|
|
433
|
+
ActionError,
|
|
434
|
+
DYNAMIC_CONTRACT_SERVICE,
|
|
435
|
+
RenderPipeline,
|
|
436
|
+
badRequest,
|
|
76
437
|
clientError4xx,
|
|
77
438
|
createJayService,
|
|
439
|
+
forbidden,
|
|
440
|
+
isJayAction,
|
|
441
|
+
isJayInit,
|
|
442
|
+
makeContractGenerator,
|
|
443
|
+
makeJayAction,
|
|
444
|
+
makeJayInit,
|
|
445
|
+
makeJayQuery,
|
|
78
446
|
makeJayStackComponent,
|
|
79
447
|
notFound,
|
|
80
448
|
partialRender,
|
|
449
|
+
phaseOutput,
|
|
81
450
|
redirect3xx,
|
|
82
|
-
serverError5xx
|
|
451
|
+
serverError5xx,
|
|
452
|
+
unauthorized
|
|
83
453
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/fullstack-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"test:watch": "vitest"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@jay-framework/component": "^0.
|
|
30
|
-
"@jay-framework/runtime": "^0.
|
|
29
|
+
"@jay-framework/component": "^0.11.0",
|
|
30
|
+
"@jay-framework/runtime": "^0.11.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@jay-framework/dev-environment": "^0.
|
|
34
|
-
"@jay-framework/jay-cli": "^0.
|
|
33
|
+
"@jay-framework/dev-environment": "^0.11.0",
|
|
34
|
+
"@jay-framework/jay-cli": "^0.11.0",
|
|
35
35
|
"@types/express": "^5.0.2",
|
|
36
36
|
"@types/node": "^22.15.21",
|
|
37
37
|
"nodemon": "^3.0.3",
|