@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/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 redirect3xx(status, location) {
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: "redirect",
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 { kind: "PartialRender", rendered, carryForward };
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.9.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.9.0",
30
- "@jay-framework/runtime": "^0.9.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.9.0",
34
- "@jay-framework/jay-cli": "^0.9.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",