@atom-forge/expose 0.0.0-stage → 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README-AI.md +85 -0
- package/README.md +31 -2
- package/dist/index.d.ts +43 -0
- package/dist/index.js +24 -0
- package/docs/en/index.md +266 -0
- package/docs/hu/index.md +266 -0
- package/package.json +62 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-11-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial package setup: TypeScript sources built with tsup, Bun tests, CI, and releases with `@atom-forge/ship`.
|
|
14
|
+
- AtomForge agent instructions (`AGENTS.md`, `AGENTS/`), the documentation skeleton (book, `docs/en`, `docs/hu`, `README-AI.md`), `TODO.md`, and `notes/`.
|
|
15
|
+
- The allow-list API: `expose`, `compose`, `fieldsOf` for naming plain fields, and the `View`, `Part`, `Stacked` and `Merge` types. A view that returns anything but a plain object is a compile error. An application gathers its views into a plain `views` object.
|
|
16
|
+
- Releases: a tag-triggered publish workflow with npm Trusted Publishing, and public `publishConfig`.
|
|
17
|
+
- The documentation of the API: book chapters, English and Hungarian docs, `README.md`, `README-AI.md`, and AI Notes.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gergely Laborci
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README-AI.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# AI README: @atom-forge/expose
|
|
2
|
+
|
|
3
|
+
A typed allow-list layer between server data and the client: nothing goes out unless a *view* names it. Use it when a server returns database rows to a client. It has no dependencies and works with any database layer or transport.
|
|
4
|
+
|
|
5
|
+
The [documentation](./docs/en/index.md) is authoritative where the two disagree; see [README.md](./README.md) for installation.
|
|
6
|
+
|
|
7
|
+
## Rules
|
|
8
|
+
|
|
9
|
+
- Copy plain fields with `fieldsOf`; write a view by hand for computed fields and nested data.
|
|
10
|
+
- A view names every field it returns. Never spread `source`, and never return a nested row as it is: run it through its own view.
|
|
11
|
+
- A view says *what* goes out, not *who* may get it. Do the permission check before the call.
|
|
12
|
+
- A view is synchronous and returns a plain object; anything else is a compile error.
|
|
13
|
+
- A later view overwrites earlier keys. To add to a key instead, read it from `output`, build a new value, and mark it `Stacked`.
|
|
14
|
+
- `output` is frozen only at the top level: do not write into its nested values.
|
|
15
|
+
- The allowed list of `fieldsOf<S>()` exists only in the types: never pass it names from a request, and leave objects off `S`.
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
type View<S, O, R extends Part> = (source: S, output: Readonly<O>) => R;
|
|
21
|
+
type Part; // a plain object: promises, arrays, strings, dates, maps and functions are compile errors
|
|
22
|
+
type Stacked<T>; // marks a key that adds up; types only, the view does the adding
|
|
23
|
+
type Merge<A, B>; // keys of B overwrite keys of A; Stacked keys become A[K] & B[K]
|
|
24
|
+
|
|
25
|
+
// 1–4 views. null and undefined become null without running a view;
|
|
26
|
+
// the result type includes null only if the type of source allows it.
|
|
27
|
+
function expose(source, a, b?, c?, d?): Output | null;
|
|
28
|
+
|
|
29
|
+
// 2–3 views. The result is a view; its source type requires the fields of all its views.
|
|
30
|
+
// For more views than the limit, compose a composed view.
|
|
31
|
+
function compose(a, b, c?): View;
|
|
32
|
+
|
|
33
|
+
// Any number of names of S. The row needs only the named fields.
|
|
34
|
+
function fieldsOf<S>(): <K extends keyof S>(...names: K[]) => (source: Pick<S, K>) => Pick<S, K>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Minimal example
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { compose, expose } from '@atom-forge/expose';
|
|
41
|
+
|
|
42
|
+
const identity = (source: { id: string; name: string }) => ({
|
|
43
|
+
id: source.id,
|
|
44
|
+
name: source.name,
|
|
45
|
+
});
|
|
46
|
+
const contact = (source: { email: string }) => ({ email: source.email });
|
|
47
|
+
const card = compose(identity, contact);
|
|
48
|
+
|
|
49
|
+
const row = { id: 'u1', name: 'Ada', email: 'ada@example.com', passwordHash: 'secret' };
|
|
50
|
+
|
|
51
|
+
const output = expose(row, card);
|
|
52
|
+
// { id: 'u1', name: 'Ada', email: 'ada@example.com' }; passwordHash is not on the type either
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Common extensions
|
|
56
|
+
|
|
57
|
+
Name plain fields with `fieldsOf`, and gather an application's views into a plain `views` object, with the views per entity in their own files:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export const fields = fieldsOf<{ id: string; name: string; email: string }>(); // server/views/user.ts
|
|
61
|
+
export const views = { user }; // import * as user from './user'
|
|
62
|
+
return rows.map(row => expose(row, views.user.fields('id', 'name')));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A view that adds to a key:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const links = <N extends string>(...names: N[]) =>
|
|
69
|
+
(source: { links: Record<string, string> }, output: Readonly<Record<string, any>>) => {
|
|
70
|
+
const picked = {} as Partial<Record<N, string>>;
|
|
71
|
+
for (const name of names) if (Object.prototype.hasOwnProperty.call(source.links, name)) picked[name] = source.links[name];
|
|
72
|
+
return { links: { ...output.links, ...picked } as Stacked<Partial<Record<N, string>>> };
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Type its `output` as `Readonly<Record<string, any>>`, so it fits after any earlier view.
|
|
77
|
+
|
|
78
|
+
## Pitfalls
|
|
79
|
+
|
|
80
|
+
- The library cannot stop code from returning a row without calling it; a project that needs this adds its own guard test.
|
|
81
|
+
- Without the `Stacked` mark, the types say the key was replaced, even though the view added to it.
|
|
82
|
+
|
|
83
|
+
## Documentation map
|
|
84
|
+
|
|
85
|
+
[`docs/en/index.md`](./docs/en/index.md) covers getting started, views, organizing an application, the API reference, the pitfalls, and upgrading. The Hungarian translation is [`docs/hu/index.md`](./docs/hu/index.md).
|
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @atom-forge/expose
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A typed allow-list layer between server data and the client: nothing goes out unless a *view* names it, so a new column stays on the server by default.
|
|
4
|
+
|
|
5
|
+
For coding assistants, see [README-AI.md](./README-AI.md).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @atom-forge/expose
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { compose, expose } from '@atom-forge/expose';
|
|
17
|
+
|
|
18
|
+
const identity = (source: { id: string; name: string }) => ({ id: source.id, name: source.name });
|
|
19
|
+
const contact = (source: { email: string }) => ({ email: source.email });
|
|
20
|
+
const card = compose(identity, contact);
|
|
21
|
+
|
|
22
|
+
const row = { id: 'u1', name: 'Ada', email: 'ada@example.com', passwordHash: 'secret' };
|
|
23
|
+
|
|
24
|
+
expose(row, card);
|
|
25
|
+
// { id: 'u1', name: 'Ada', email: 'ada@example.com' }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
- [The Expose book](https://books.atom-forge.eu) — the full documentation as a multi-page book.
|
|
31
|
+
- [English documentation](docs/en/index.md) ([magyarul](docs/hu/index.md)) — the same documentation shipped with the package, including the upgrade notes.
|
|
32
|
+
- [AI guide](README-AI.md) — the entry point for coding agents using the package.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allow-list layer between server data and the client: nothing leaves unless a view names it.
|
|
3
|
+
*
|
|
4
|
+
* A view receives the source row and what the earlier views built, and returns only its own part. Later parts
|
|
5
|
+
* overwrite earlier keys; a view that wants to add to a key reads it from `output` and marks it `Stacked`.
|
|
6
|
+
* Never spread `source` in a view: that would let every column out.
|
|
7
|
+
*/
|
|
8
|
+
type View<S, O, R extends Part> = (source: S, output: Readonly<O>) => R;
|
|
9
|
+
/**
|
|
10
|
+
* What a view may return: a plain object. The run-time merge is an object spread, so a promise would lose its data and
|
|
11
|
+
* an array or a string would become numbered keys. Only symbol keys are excluded, so no field name can collide.
|
|
12
|
+
*/
|
|
13
|
+
type Part = object & {
|
|
14
|
+
readonly [Symbol.iterator]?: never;
|
|
15
|
+
readonly [Symbol.toStringTag]?: never;
|
|
16
|
+
readonly [Symbol.hasInstance]?: never;
|
|
17
|
+
readonly [Symbol.toPrimitive]?: never;
|
|
18
|
+
};
|
|
19
|
+
declare const stacked: unique symbol;
|
|
20
|
+
/** Marks a key that adds to the earlier value instead of replacing it; the view does the adding with `output`. */
|
|
21
|
+
type Stacked<T> = T & {
|
|
22
|
+
readonly [stacked]?: true;
|
|
23
|
+
};
|
|
24
|
+
/** The keys of `B` overwrite the keys of `A`, as the merge at run time does; `Stacked` keys add up. */
|
|
25
|
+
type Merge<A, B> = {
|
|
26
|
+
[K in keyof A | keyof B]: K extends keyof B ? K extends keyof A ? (typeof stacked extends keyof B[K] ? A[K] & B[K] : B[K]) : B[K] : K extends keyof A ? A[K] : never;
|
|
27
|
+
};
|
|
28
|
+
type Nullish<S> = null extends S ? null : undefined extends S ? null : never;
|
|
29
|
+
/** Builds the output of a row (null and undefined become null, so an optional relation needs no check at the call). */
|
|
30
|
+
declare function expose<S, A extends Part>(source: S, a: View<NonNullable<S>, {}, A>): A | Nullish<S>;
|
|
31
|
+
declare function expose<S, A extends Part, B extends Part>(source: S, a: View<NonNullable<S>, {}, A>, b: View<NonNullable<S>, A, B>): Merge<A, B> | Nullish<S>;
|
|
32
|
+
declare function expose<S, A extends Part, B extends Part, C extends Part>(source: S, a: View<NonNullable<S>, {}, A>, b: View<NonNullable<S>, A, B>, c: View<NonNullable<S>, Merge<A, B>, C>): Merge<Merge<A, B>, C> | Nullish<S>;
|
|
33
|
+
declare function expose<S, A extends Part, B extends Part, C extends Part, D extends Part>(source: S, a: View<NonNullable<S>, {}, A>, b: View<NonNullable<S>, A, B>, c: View<NonNullable<S>, Merge<A, B>, C>, d: View<NonNullable<S>, Merge<Merge<A, B>, C>, D>): Merge<Merge<Merge<A, B>, C>, D> | Nullish<S>;
|
|
34
|
+
/** Names a combination of views; the result is a view itself. */
|
|
35
|
+
declare function compose<S1, A extends Part, S2, B extends Part>(a: View<S1, {}, A>, b: View<S2, A, B>): View<S1 & S2, {}, Merge<A, B>>;
|
|
36
|
+
declare function compose<S1, A extends Part, S2, B extends Part, S3, C extends Part>(a: View<S1, {}, A>, b: View<S2, A, B>, c: View<S3, Merge<A, B>, C>): View<S1 & S2 & S3, {}, Merge<Merge<A, B>, C>>;
|
|
37
|
+
/**
|
|
38
|
+
* Makes a view factory for an entity: `S` lists the fields that may go out by name, the factory takes the names.
|
|
39
|
+
* The list exists only in the types: never pass it names from a request, and leave objects off `S`.
|
|
40
|
+
*/
|
|
41
|
+
declare function fieldsOf<S>(): <K extends keyof S>(...names: K[]) => (source: Pick<S, K>) => Pick<S, K>;
|
|
42
|
+
|
|
43
|
+
export { type Merge, type Part, type Stacked, type View, compose, expose, fieldsOf };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function run(source, views, start) {
|
|
3
|
+
let output = start;
|
|
4
|
+
for (const view of views) output = { ...output, ...view(source, Object.freeze(output)) };
|
|
5
|
+
return output;
|
|
6
|
+
}
|
|
7
|
+
function expose(source, ...views) {
|
|
8
|
+
return source === null || source === void 0 ? null : run(source, views, {});
|
|
9
|
+
}
|
|
10
|
+
function compose(...views) {
|
|
11
|
+
return (source, output) => run(source, views, output);
|
|
12
|
+
}
|
|
13
|
+
function fieldsOf() {
|
|
14
|
+
return (...names) => (source) => {
|
|
15
|
+
const picked = {};
|
|
16
|
+
for (const name of names) picked[name] = source[name];
|
|
17
|
+
return picked;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
compose,
|
|
22
|
+
expose,
|
|
23
|
+
fieldsOf
|
|
24
|
+
};
|
package/docs/en/index.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<!-- Derived from the book in book/ (https://books.atom-forge.eu). Change the book first, then update this file and docs/hu/index.md. -->
|
|
2
|
+
|
|
3
|
+
# Expose
|
|
4
|
+
|
|
5
|
+
`@atom-forge/expose` is a small TypeScript library that decides what leaves the server: nothing goes to the client unless a *view* names it. When a table gets a new column, that column stays on the server until someone adds it to a view, and the compiler knows the exact shape of what went out.
|
|
6
|
+
|
|
7
|
+
It has no dependencies and works with any database layer or transport, because a view is a plain function.
|
|
8
|
+
|
|
9
|
+
## Getting started
|
|
10
|
+
|
|
11
|
+
### Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add @atom-forge/expose
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package is ESM-only and ships its own type declarations.
|
|
18
|
+
|
|
19
|
+
### Write a view and build an output
|
|
20
|
+
|
|
21
|
+
A view receives the source row and returns the part it is responsible for, naming every field. `expose` runs the views on a row and merges what they return:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { expose } from '@atom-forge/expose';
|
|
25
|
+
|
|
26
|
+
const identity = (source: { id: string; name: string }) => ({
|
|
27
|
+
id: source.id,
|
|
28
|
+
name: source.name,
|
|
29
|
+
});
|
|
30
|
+
const contact = (source: { email: string }) => ({ email: source.email });
|
|
31
|
+
|
|
32
|
+
const row = { id: 'u1', name: 'Ada', email: 'ada@example.com', passwordHash: 'secret' };
|
|
33
|
+
|
|
34
|
+
const output = expose(row, identity, contact);
|
|
35
|
+
// { id: 'u1', name: 'Ada', email: 'ada@example.com' }
|
|
36
|
+
|
|
37
|
+
output.email; // string
|
|
38
|
+
// output.passwordHash; // compile error: it is not on the output
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`passwordHash` stays on the server because no view names it. The same holds for any column added to the table later.
|
|
42
|
+
|
|
43
|
+
### Naming plain fields
|
|
44
|
+
|
|
45
|
+
For fields copied as they are, `fieldsOf` saves writing the view by hand. Give it a type that lists what may go out by name, then name the fields at the call:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { expose, fieldsOf } from '@atom-forge/expose';
|
|
49
|
+
|
|
50
|
+
type PublicUser = { id: string; name: string; email: string };
|
|
51
|
+
const fields = fieldsOf<PublicUser>();
|
|
52
|
+
|
|
53
|
+
const output = expose(row, fields('id', 'name'));
|
|
54
|
+
// { id: 'u1', name: 'Ada' }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Rows that may be missing
|
|
58
|
+
|
|
59
|
+
`null` and `undefined` become `null` without running a view, so an optional relation needs no check at the call. The result type includes `null` only when the source type can be missing:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const author = row as typeof row | null;
|
|
63
|
+
const output = expose(author, identity); // { id: string; name: string } | null
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Views
|
|
67
|
+
|
|
68
|
+
A view has the shape `(source, output) => ownPart`. `source` is the row, and `output` is what the earlier views have built. The view returns only its own part; `expose` does the merging.
|
|
69
|
+
|
|
70
|
+
### Naming fields with `fieldsOf`
|
|
71
|
+
|
|
72
|
+
`fieldsOf<S>()` makes a view factory for an entity. `S` lists the fields that may go out by name; the factory takes the names and returns a view. Use it for fields copied as they are, and write a view by hand for computed fields and nested data.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { expose, fieldsOf } from '@atom-forge/expose';
|
|
76
|
+
|
|
77
|
+
type PublicUser = { id: string; name: string; email: string };
|
|
78
|
+
const fields = fieldsOf<PublicUser>();
|
|
79
|
+
|
|
80
|
+
const output = expose(row, fields('id', 'name'));
|
|
81
|
+
// { id: 'u1', name: 'Ada' }
|
|
82
|
+
|
|
83
|
+
output.email; // compile error: it was not named
|
|
84
|
+
fields('nmae'); // compile error: no such field
|
|
85
|
+
fields('passwordHash'); // compile error: it is not on PublicUser
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The row needs only the named fields, so a query that read just some of the columns fits, and naming a column it did not read is a compile error:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const partial = { id: 'u1', name: 'Ada' };
|
|
92
|
+
|
|
93
|
+
expose(partial, fields('id', 'name'));
|
|
94
|
+
expose(partial, fields('email')); // compile error: the row has no email
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The number of names is not limited. The result is a view like any other, so it can be composed and combined with other views:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
expose(row, fields('id', 'name'), contact);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The empty call in `fieldsOf<PublicUser>()` is needed because TypeScript cannot take one type argument by hand and infer another.
|
|
104
|
+
|
|
105
|
+
### Later views overwrite
|
|
106
|
+
|
|
107
|
+
The parts merge from left to right, so a later key replaces an earlier one, in the types as well:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const output = expose(row, identity, () => ({ name: 'Anonymous' }));
|
|
111
|
+
// { id: 'u1', name: 'Anonymous' }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Reading the earlier output
|
|
115
|
+
|
|
116
|
+
A view can build on what is already there. `output` is frozen, so a view cannot change it in place:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const label = (_source: unknown, output: Readonly<{ name: string; email: string }>) => ({
|
|
120
|
+
label: `${output.name} <${output.email}>`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expose(row, identity, contact, label);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Composing views
|
|
127
|
+
|
|
128
|
+
`compose` names a combination of views. The result is a view, so it can be passed to `expose` or composed further. Its source type requires the fields of all of its views:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { compose } from '@atom-forge/expose';
|
|
132
|
+
|
|
133
|
+
const card = compose(identity, contact);
|
|
134
|
+
const output = expose(row, card);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A composed view keeps what earlier views built, like any other view.
|
|
138
|
+
|
|
139
|
+
### Adding to a key with `Stacked`
|
|
140
|
+
|
|
141
|
+
Sometimes several views contribute to one key, for example when each one picks a few named links. Such a view reads the key from `output`, returns the combined value, and marks it with `Stacked`. The mark tells the types that the key adds up instead of being replaced; the view itself does the adding at run time.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { expose, type Stacked } from '@atom-forge/expose';
|
|
145
|
+
|
|
146
|
+
const links = <N extends string>(...names: N[]) =>
|
|
147
|
+
(source: { links: Record<string, string> }, output: Readonly<Record<string, any>>) => {
|
|
148
|
+
const picked = {} as Partial<Record<N, string>>;
|
|
149
|
+
for (const name of names) if (Object.prototype.hasOwnProperty.call(source.links, name)) picked[name] = source.links[name];
|
|
150
|
+
return { links: { ...output.links, ...picked } as Stacked<Partial<Record<N, string>>> };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const row = { links: { home: 'h', work: 'w', private: 'p' } as Record<string, string> };
|
|
154
|
+
|
|
155
|
+
const output = expose(row, links('home'), links('work'));
|
|
156
|
+
// { links: { home: 'h', work: 'w' } }
|
|
157
|
+
// output.links.private; // compile error: it was not named
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Declare `output` as `Readonly<Record<string, any>>` in such a view, so that it fits after any earlier view.
|
|
161
|
+
|
|
162
|
+
## Organizing an application
|
|
163
|
+
|
|
164
|
+
The library knows nothing about your entities. A practical layout keeps the views of each entity in one file and gathers them into a plain `views` object, so every call site reads the same way.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// server/views/user.ts
|
|
168
|
+
import { fieldsOf } from '@atom-forge/expose';
|
|
169
|
+
|
|
170
|
+
type PublicUser = { id: string; name: string; email: string };
|
|
171
|
+
|
|
172
|
+
export const fields = fieldsOf<PublicUser>();
|
|
173
|
+
export const privateSalary = (source: { salary: number }) => ({ salary: source.salary });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// server/views/index.ts
|
|
178
|
+
import * as user from './user';
|
|
179
|
+
|
|
180
|
+
export const views = { user };
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// at a call site
|
|
185
|
+
import { expose } from '@atom-forge/expose';
|
|
186
|
+
import { views } from '../views';
|
|
187
|
+
|
|
188
|
+
return rows.map(row => expose(row, views.user.fields('id', 'name')));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Who, and what
|
|
192
|
+
|
|
193
|
+
Keep two questions apart. The module decides *who* may get the data, with its usual permission check. A view only says *what* goes out, and it cannot check anything about the caller. When a view carries sensitive data, make that visible in its name and call it only after the check:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
assertCanReadSalary(viewer, row); // your own permission check
|
|
197
|
+
return expose(row, views.user.fields('id', 'name'), views.user.privateSalary);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Keeping raw rows out
|
|
201
|
+
|
|
202
|
+
The library cannot stop code from returning a row without calling `expose`. If that matters in your project, add a test that scans the server code for the outputs you want to forbid, with an explicit allow-list for the exceptions.
|
|
203
|
+
|
|
204
|
+
## API reference
|
|
205
|
+
|
|
206
|
+
### `expose(source, ...views)`
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
function expose<S, A>(source: S, a: View<NonNullable<S>, {}, A>): A | Nullish<S>;
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Runs the views on `source` from left to right and returns the merged output. It takes one to four views; the signature above shows one. For more, combine some with `compose`.
|
|
213
|
+
|
|
214
|
+
If `source` is `null` or `undefined`, it returns `null` and runs no view. The result type includes `null` only when the type of `source` allows a missing value.
|
|
215
|
+
|
|
216
|
+
### `compose(...views)`
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
function compose<S1, A, S2, B>(a: View<S1, {}, A>, b: View<S2, A, B>): View<S1 & S2, {}, Merge<A, B>>;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Returns a new view that runs the given views in order, on top of the output it receives. It takes two or three views; the signature above shows two. For more, compose a composed view.
|
|
223
|
+
|
|
224
|
+
### `fieldsOf<S>()`
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
function fieldsOf<S>(): <K extends keyof S>(...names: K[]) => (source: Pick<S, K>) => Pick<S, K>;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Returns a factory that takes field names of `S` and returns a view copying those fields. The list of allowed fields exists only in the types: at run time the view copies the names it receives.
|
|
231
|
+
|
|
232
|
+
### `View<S, O, R>`
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
type View<S, O, R extends Part> = (source: S, output: Readonly<O>) => R;
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`S` is the source the view reads, `O` is the output built before it, and `R` is the part it returns. `R` must be a plain object: a promise, an array, a string, a date, a map, a function or a possibly `null` value is a compile error, because it could not be merged.
|
|
239
|
+
|
|
240
|
+
### `Part`
|
|
241
|
+
|
|
242
|
+
What a view may return: a plain object. Only symbol keys are excluded, so a field named `then` or `length` is fine.
|
|
243
|
+
|
|
244
|
+
### `Stacked<T>`
|
|
245
|
+
|
|
246
|
+
Marks the type of a key that adds to the earlier value instead of replacing it. It affects the types only; the view does the adding with `output`.
|
|
247
|
+
|
|
248
|
+
### `Merge<A, B>`
|
|
249
|
+
|
|
250
|
+
The type of merging `B` over `A`: the keys of `B` overwrite the keys of `A`, and `Stacked` keys become `A[K] & B[K]`.
|
|
251
|
+
|
|
252
|
+
## Pitfalls
|
|
253
|
+
|
|
254
|
+
The allow-list holds only as far as the views name their fields. These are the things the library cannot check for you.
|
|
255
|
+
|
|
256
|
+
- **Spreading the source.** `({ ...source })` lets every column out, including those added later. Name each field.
|
|
257
|
+
- **Returning a nested object as it is.** `({ author: source.author })` sends the whole `author` row. Run it through its own view: `author: expose(source.author, identity)`.
|
|
258
|
+
- **Passing names from a request to `fields`.** The list of allowed fields in `fieldsOf<S>()` exists only in the types. `fields(...request.fields)` with a cast copies whatever the caller asks for. Name the fields in the code.
|
|
259
|
+
- **Listing an object in the type of `fieldsOf`.** A field on that type goes out as it is, so an object goes out whole, with every key it gets later. Leave it off and write a view for it.
|
|
260
|
+
- **Treating a view as a permission check.** A view says what goes out, not who may get it. Do the permission check before the call.
|
|
261
|
+
- **Changing a nested value of `output`.** `output` is frozen only at the top level. A view that writes into `output.links` changes an earlier view's value. Build a new object instead, as in the stacking example.
|
|
262
|
+
- **Forgetting the `Stacked` mark.** Without it, the types say the key was replaced even though your view added to it, so the earlier names are missing from the output type.
|
|
263
|
+
|
|
264
|
+
## Upgrading
|
|
265
|
+
|
|
266
|
+
0.1.0 is the first published version, so there is nothing to upgrade from yet.
|
package/docs/hu/index.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<!-- A docs/en/index.md magyar fordítása. Előbb a book és a docs/en változik, utána ez a fájl. -->
|
|
2
|
+
|
|
3
|
+
# Expose
|
|
4
|
+
|
|
5
|
+
Az `@atom-forge/expose` egy kis TypeScript-könyvtár, amely azt szabályozza, mi hagyhatja el a szervert: semmi nem jut el a klienshez, amit egy *nézet* (view) meg nem nevez. Ha egy tábla új oszlopot kap, az mindaddig a szerveren marad, amíg valaki hozzá nem adja egy nézethez, és a fordító pontosan ismeri a kimenő adat alakját.
|
|
6
|
+
|
|
7
|
+
Nincs függősége, és bármilyen adatbázisréteggel vagy átviteli móddal használható, mert a nézet egy egyszerű függvény.
|
|
8
|
+
|
|
9
|
+
## Első lépések
|
|
10
|
+
|
|
11
|
+
### Telepítés
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add @atom-forge/expose
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A csomag csak ESM-ként érhető el, és tartalmazza a saját típusdeklarációit.
|
|
18
|
+
|
|
19
|
+
### Nézet írása és a kimenet felépítése
|
|
20
|
+
|
|
21
|
+
A nézet megkapja a forrássort, és visszaadja azt a részt, amelyért felel, minden mezőt megnevezve. Az `expose` lefuttatja a nézeteket a soron, és összefésüli, amit visszaadnak:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { expose } from '@atom-forge/expose';
|
|
25
|
+
|
|
26
|
+
const identity = (source: { id: string; name: string }) => ({
|
|
27
|
+
id: source.id,
|
|
28
|
+
name: source.name,
|
|
29
|
+
});
|
|
30
|
+
const contact = (source: { email: string }) => ({ email: source.email });
|
|
31
|
+
|
|
32
|
+
const row = { id: 'u1', name: 'Ada', email: 'ada@example.com', passwordHash: 'secret' };
|
|
33
|
+
|
|
34
|
+
const output = expose(row, identity, contact);
|
|
35
|
+
// { id: 'u1', name: 'Ada', email: 'ada@example.com' }
|
|
36
|
+
|
|
37
|
+
output.email; // string
|
|
38
|
+
// output.passwordHash; // fordítási hiba: nincs a kimeneten
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A `passwordHash` a szerveren marad, mert egyetlen nézet sem nevezi meg. Ugyanez igaz minden oszlopra, amelyet később adnak a táblához.
|
|
42
|
+
|
|
43
|
+
### Egyszerű mezők megnevezése
|
|
44
|
+
|
|
45
|
+
Változatlanul másolt mezőknél a `fieldsOf` megspórolja a nézet kézi megírását. Adj meg neki egy típust, amely felsorolja a név szerint kiadható mezőket, majd a hívásnál nevezd meg őket:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { expose, fieldsOf } from '@atom-forge/expose';
|
|
49
|
+
|
|
50
|
+
type PublicUser = { id: string; name: string; email: string };
|
|
51
|
+
const fields = fieldsOf<PublicUser>();
|
|
52
|
+
|
|
53
|
+
const output = expose(row, fields('id', 'name'));
|
|
54
|
+
// { id: 'u1', name: 'Ada' }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Hiányozható sorok
|
|
58
|
+
|
|
59
|
+
A `null` és az `undefined` nézet futtatása nélkül `null` lesz, így egy opcionális kapcsolatnál nem kell ellenőrzés a hívás helyén. Az eredmény típusa csak akkor tartalmazza a `null`-t, ha a forrás típusa megengedi a hiányt:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const author = row as typeof row | null;
|
|
63
|
+
const output = expose(author, identity); // { id: string; name: string } | null
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Nézetek
|
|
67
|
+
|
|
68
|
+
A nézet alakja `(source, output) => sajátRész`. A `source` a sor, az `output` pedig az, amit a korábbi nézetek felépítettek. A nézet csak a saját részét adja vissza; az összefésülést az `expose` végzi.
|
|
69
|
+
|
|
70
|
+
### Mezők megnevezése a `fieldsOf` függvénnyel
|
|
71
|
+
|
|
72
|
+
A `fieldsOf<S>()` nézetgyárat készít egy entitáshoz. Az `S` felsorolja a név szerint kiadható mezőket; a gyár a neveket kapja meg, és egy nézetet ad vissza. Változatlanul másolt mezőkhöz használd, számolt mezőkhöz és beágyazott adatokhoz pedig írj kézzel nézetet.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { expose, fieldsOf } from '@atom-forge/expose';
|
|
76
|
+
|
|
77
|
+
type PublicUser = { id: string; name: string; email: string };
|
|
78
|
+
const fields = fieldsOf<PublicUser>();
|
|
79
|
+
|
|
80
|
+
const output = expose(row, fields('id', 'name'));
|
|
81
|
+
// { id: 'u1', name: 'Ada' }
|
|
82
|
+
|
|
83
|
+
output.email; // fordítási hiba: nem lett megnevezve
|
|
84
|
+
fields('nmae'); // fordítási hiba: nincs ilyen mező
|
|
85
|
+
fields('passwordHash'); // fordítási hiba: nincs a PublicUser típusban
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A sortól csak a megnevezett mezőket kéri, így egy csak néhány oszlopot beolvasó lekérdezés is megfelel, a be nem olvasott oszlop megnevezése pedig fordítási hiba:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const partial = { id: 'u1', name: 'Ada' };
|
|
92
|
+
|
|
93
|
+
expose(partial, fields('id', 'name'));
|
|
94
|
+
expose(partial, fields('email')); // fordítási hiba: a sorban nincs email
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A nevek száma nincs korlátozva. Az eredmény ugyanolyan nézet, mint a többi, tehát összeállítható és kombinálható más nézetekkel:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
expose(row, fields('id', 'name'), contact);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A `fieldsOf<PublicUser>()` üres hívására azért van szükség, mert a TypeScript nem enged egy típusparamétert kézzel megadni, a másikat pedig kikövetkeztetni.
|
|
104
|
+
|
|
105
|
+
### A későbbi nézetek felülírnak
|
|
106
|
+
|
|
107
|
+
A részek balról jobbra fésülődnek össze, így a későbbi kulcs lecseréli a korábbit, a típusokban is:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const output = expose(row, identity, () => ({ name: 'Anonymous' }));
|
|
111
|
+
// { id: 'u1', name: 'Anonymous' }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### A korábbi kimenet olvasása
|
|
115
|
+
|
|
116
|
+
Egy nézet építhet arra, ami már megvan. Az `output` fagyasztott, így a nézet nem tudja helyben módosítani:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const label = (_source: unknown, output: Readonly<{ name: string; email: string }>) => ({
|
|
120
|
+
label: `${output.name} <${output.email}>`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expose(row, identity, contact, label);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Nézetek összeállítása
|
|
127
|
+
|
|
128
|
+
A `compose` nevet ad nézetek egy kombinációjának. Az eredmény maga is nézet, tehát átadható az `expose`-nak, vagy tovább kombinálható. A forrástípusa az összes nézete mezőit megköveteli:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { compose } from '@atom-forge/expose';
|
|
132
|
+
|
|
133
|
+
const card = compose(identity, contact);
|
|
134
|
+
const output = expose(row, card);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Az összeállított nézet megtartja, amit a korábbi nézetek felépítettek, mint bármely más nézet.
|
|
138
|
+
|
|
139
|
+
### Hozzáadás egy kulcshoz a `Stacked` típussal
|
|
140
|
+
|
|
141
|
+
Néha több nézet is hozzájárul ugyanahhoz a kulcshoz, például amikor mindegyik néhány megnevezett linket választ ki. Az ilyen nézet kiolvassa a kulcsot az `output`-ból, visszaadja az egyesített értéket, és `Stacked` jelöléssel látja el. A jelölés azt mondja a típusoknak, hogy a kulcs összeadódik, nem cserélődik; futásidőben az összeadást maga a nézet végzi.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { expose, type Stacked } from '@atom-forge/expose';
|
|
145
|
+
|
|
146
|
+
const links = <N extends string>(...names: N[]) =>
|
|
147
|
+
(source: { links: Record<string, string> }, output: Readonly<Record<string, any>>) => {
|
|
148
|
+
const picked = {} as Partial<Record<N, string>>;
|
|
149
|
+
for (const name of names) if (Object.prototype.hasOwnProperty.call(source.links, name)) picked[name] = source.links[name];
|
|
150
|
+
return { links: { ...output.links, ...picked } as Stacked<Partial<Record<N, string>>> };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const row = { links: { home: 'h', work: 'w', private: 'p' } as Record<string, string> };
|
|
154
|
+
|
|
155
|
+
const output = expose(row, links('home'), links('work'));
|
|
156
|
+
// { links: { home: 'h', work: 'w' } }
|
|
157
|
+
// output.links.private; // fordítási hiba: nem lett megnevezve
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Az ilyen nézetben az `output` típusa legyen `Readonly<Record<string, any>>`, hogy bármely korábbi nézet után használható legyen.
|
|
161
|
+
|
|
162
|
+
## Egy alkalmazás felépítése
|
|
163
|
+
|
|
164
|
+
A könyvtár semmit nem tud az entitásaidról. Bevált felépítés, ha minden entitás nézetei egy fájlban vannak, és egy közönséges `views` objektumba gyűlnek, így minden hívás ugyanúgy olvasható.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// server/views/user.ts
|
|
168
|
+
import { fieldsOf } from '@atom-forge/expose';
|
|
169
|
+
|
|
170
|
+
type PublicUser = { id: string; name: string; email: string };
|
|
171
|
+
|
|
172
|
+
export const fields = fieldsOf<PublicUser>();
|
|
173
|
+
export const privateSalary = (source: { salary: number }) => ({ salary: source.salary });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// server/views/index.ts
|
|
178
|
+
import * as user from './user';
|
|
179
|
+
|
|
180
|
+
export const views = { user };
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// a hívás helyén
|
|
185
|
+
import { expose } from '@atom-forge/expose';
|
|
186
|
+
import { views } from '../views';
|
|
187
|
+
|
|
188
|
+
return rows.map(row => expose(row, views.user.fields('id', 'name')));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Ki és mit
|
|
192
|
+
|
|
193
|
+
Két kérdést tarts külön. A modul dönti el, *ki* kaphatja meg az adatot, a szokásos jogosultság-ellenőrzésével. A nézet csak azt mondja meg, *mi* megy ki, és semmit nem tud ellenőrizni a hívóról. Ha egy nézet érzékeny adatot visz, ez látsszon a nevén, és csak az ellenőrzés után hívd:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
assertCanReadSalary(viewer, row); // a saját jogosultság-ellenőrzésed
|
|
197
|
+
return expose(row, views.user.fields('id', 'name'), views.user.privateSalary);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### A nyers sorok távol tartása
|
|
201
|
+
|
|
202
|
+
A könyvtár nem tudja megakadályozni, hogy a kód `expose` hívása nélkül adjon vissza egy sort. Ha ez a projektedben számít, írj egy tesztet, amely megkeresi a szerverkódban a tiltani kívánt kimeneteket, a kivételekhez pedig explicit engedélylistát használ.
|
|
203
|
+
|
|
204
|
+
## API-referencia
|
|
205
|
+
|
|
206
|
+
### `expose(source, ...views)`
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
function expose<S, A>(source: S, a: View<NonNullable<S>, {}, A>): A | Nullish<S>;
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Balról jobbra lefuttatja a nézeteket a `source` értéken, és visszaadja az összefésült kimenetet. Egy–négy nézetet fogad; a fenti aláírás az egynézetes alakot mutatja. Ennél többhöz kombináld őket a `compose` függvénnyel.
|
|
213
|
+
|
|
214
|
+
Ha a `source` `null` vagy `undefined`, `null`-t ad vissza, és egyetlen nézetet sem futtat. Az eredmény típusa csak akkor tartalmazza a `null`-t, ha a `source` típusa megengedi a hiányzó értéket.
|
|
215
|
+
|
|
216
|
+
### `compose(...views)`
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
function compose<S1, A, S2, B>(a: View<S1, {}, A>, b: View<S2, A, B>): View<S1 & S2, {}, Merge<A, B>>;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Új nézetet ad vissza, amely sorban lefuttatja a megadott nézeteket a kapott kimenetre építve. Két vagy három nézetet fogad; a fenti aláírás a kétnézetes alakot mutatja. Ennél többhöz egy már összeállított nézetet kombinálj tovább.
|
|
223
|
+
|
|
224
|
+
### `fieldsOf<S>()`
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
function fieldsOf<S>(): <K extends keyof S>(...names: K[]) => (source: Pick<S, K>) => Pick<S, K>;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Olyan gyárat ad vissza, amely az `S` mezőneveit kapja meg, és egy ezeket a mezőket másoló nézetet ad vissza. Az engedélyezett mezők listája csak a típusokban létezik: futásidőben a nézet azokat a neveket másolja, amelyeket megkapott.
|
|
231
|
+
|
|
232
|
+
### `View<S, O, R>`
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
type View<S, O, R extends Part> = (source: S, output: Readonly<O>) => R;
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Az `S` a nézet által olvasott forrás, az `O` az előtte felépített kimenet, az `R` pedig a visszaadott rész. Az `R` egyszerű objektum legyen: a promise, a tömb, a szöveg, a dátum, a map, a függvény és az esetleg `null` érték fordítási hiba, mert nem fésülhető össze.
|
|
239
|
+
|
|
240
|
+
### `Part`
|
|
241
|
+
|
|
242
|
+
Amit egy nézet visszaadhat: egyszerű objektum. Csak szimbólumkulcsok alapján szűr, így egy `then` vagy `length` nevű mező használható.
|
|
243
|
+
|
|
244
|
+
### `Stacked<T>`
|
|
245
|
+
|
|
246
|
+
Olyan kulcs típusát jelöli, amely a korábbi értékhez hozzáad, nem pedig lecseréli. Csak a típusokra hat; az összeadást a nézet végzi az `output` segítségével.
|
|
247
|
+
|
|
248
|
+
### `Merge<A, B>`
|
|
249
|
+
|
|
250
|
+
A `B` `A` fölé fésülésének típusa: a `B` kulcsai felülírják az `A` kulcsait, a `Stacked` kulcsok pedig `A[K] & B[K]` típusúak lesznek.
|
|
251
|
+
|
|
252
|
+
## Buktatók
|
|
253
|
+
|
|
254
|
+
Az engedélylista csak addig véd, amíg a nézetek megnevezik a mezőiket. Ezeket a könyvtár nem tudja helyetted ellenőrizni.
|
|
255
|
+
|
|
256
|
+
- **A forrás szétterítése.** A `({ ...source })` minden oszlopot kienged, a később hozzáadottakat is. Nevezz meg minden mezőt.
|
|
257
|
+
- **Beágyazott objektum változatlan visszaadása.** A `({ author: source.author })` a teljes `author` sort kiküldi. Futtasd a saját nézetén keresztül: `author: expose(source.author, identity)`.
|
|
258
|
+
- **Kérésből jövő nevek átadása a `fields` függvénynek.** A `fieldsOf<S>()` engedélyezett mezőinek listája csak a típusokban létezik. A `fields(...request.fields)` egy típuskényszerítéssel azt másolja, amit a hívó kér. A mezőket a kódban nevezd meg.
|
|
259
|
+
- **Objektum felvétele a `fieldsOf` típusába.** Az ebben a típusban szereplő mező változatlanul kimegy, így egy objektum is egészben, minden később kapott kulcsával együtt. Hagyd ki a típusból, és írj hozzá nézetet.
|
|
260
|
+
- **A nézet jogosultság-ellenőrzésként kezelése.** A nézet azt mondja meg, mi megy ki, azt nem, hogy ki kaphatja meg. A jogosultságot a hívás előtt ellenőrizd.
|
|
261
|
+
- **Az `output` beágyazott értékének módosítása.** Az `output` csak a legfelső szinten fagyasztott. Ha egy nézet az `output.links` értékébe ír, egy korábbi nézet értékét változtatja meg. Inkább új objektumot építs, ahogy a `Stacked` példában.
|
|
262
|
+
- **A `Stacked` jelölés elhagyása.** Nélküle a típusok szerint a kulcs lecserélődött, pedig a nézet hozzáadott, így a korábbi nevek hiányoznak a kimenet típusából.
|
|
263
|
+
|
|
264
|
+
## Frissítés
|
|
265
|
+
|
|
266
|
+
A 0.1.0 az első kiadott verzió, így még nincs miről frissíteni.
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@atom-forge/expose",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed allow-list layer between server data and the client: nothing goes out unless a view names it",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"allow-list",
|
|
7
|
+
"expose",
|
|
8
|
+
"serialization",
|
|
9
|
+
"security",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Gergely Laborci",
|
|
14
|
+
"url": "https://github.com/laborci"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/atom-forge/expose.git"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"docs",
|
|
34
|
+
"README-AI.md",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"prepublishOnly": "bun run typecheck && bun test && bun run build",
|
|
42
|
+
"pub-check": "ship-it check",
|
|
43
|
+
"pub": "ship-it"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"ship": {
|
|
49
|
+
"verify": [
|
|
50
|
+
"bun run typecheck",
|
|
51
|
+
"bun test",
|
|
52
|
+
"bun run build",
|
|
53
|
+
"npm pack --dry-run --ignore-scripts"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@atom-forge/ship": "file:../ship",
|
|
58
|
+
"@types/bun": "^1.4.4",
|
|
59
|
+
"@types/node": "^25.3.1",
|
|
60
|
+
"tsup": "^8.5.1",
|
|
61
|
+
"typescript": "^5.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|