@nxara/olio 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/LICENSE +21 -0
- package/README.md +335 -2
- package/dist/builders.d.ts +14 -0
- package/dist/builders.d.ts.map +1 -0
- package/dist/builders.js +18 -0
- package/dist/builders.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/olio.d.ts +21 -0
- package/dist/olio.d.ts.map +1 -0
- package/dist/olio.js +514 -0
- package/dist/olio.js.map +1 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nxara contributors
|
|
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.md
CHANGED
|
@@ -1,3 +1,336 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nxara/olio
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Modular, type-safe GraphQL applications. Each module owns its SDL, resolvers and middleware; `olio()` composes them into one executable `GraphQLSchema`. Every resolver and middleware key is checked against the `Application` type generated by [`@nxara/olio-codegen`](../codegen).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @nxara/olio graphql@16
|
|
9
|
+
bun add -d @nxara/olio-codegen
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`graphql` 16 is a peer dependency (not 17, which is about 4× slower on small queries).
|
|
13
|
+
|
|
14
|
+
## Register your app
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// src/olio.d.ts
|
|
18
|
+
import type { Application } from "./generated.js";
|
|
19
|
+
|
|
20
|
+
declare module "@nxara/olio" {
|
|
21
|
+
interface Olio {
|
|
22
|
+
application: Application;
|
|
23
|
+
ctx: { req_id: string; userId?: string };
|
|
24
|
+
// Optional: subscriptions' own context. Leave it out and they get ctx.
|
|
25
|
+
sub_ctx: { connectionId: string; userId?: string };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Modules
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// src/modules/users/index.ts
|
|
34
|
+
import { module, mw } from "@nxara/olio";
|
|
35
|
+
import { loadResolvers, loadTypeDefs } from "@nxara/olio-tools";
|
|
36
|
+
|
|
37
|
+
export const usersModule = module.users({
|
|
38
|
+
typeDefs: await loadTypeDefs(new URL("typedefs/*.gql", import.meta.url)),
|
|
39
|
+
resolvers: await loadResolvers(
|
|
40
|
+
new URL("{query,mutation,subscription}.{js,ts}", import.meta.url),
|
|
41
|
+
new URL("resolvers/*.{js,ts}", import.meta.url)
|
|
42
|
+
),
|
|
43
|
+
middleware: mw.users({ Query: { user: [cache()] } })
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Resolver files export one object per type, built with a typed helper:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// query.ts
|
|
51
|
+
export const Query = query.users({
|
|
52
|
+
user: (root, { id }, ctx, info) => findUser(id)
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// resolvers/user.ts
|
|
56
|
+
export const User = resolvers.users.User({
|
|
57
|
+
name: (user, { trim }) => user.name
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`mutation.<id>` and `subscription.<id>` work the same way. See [Subscriptions](#subscriptions) for what a subscription field looks like.
|
|
62
|
+
|
|
63
|
+
Or write the map inline, with no builders: it's typed from the module just the same.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export const usersModule = module.users({
|
|
67
|
+
typeDefs,
|
|
68
|
+
resolvers: {
|
|
69
|
+
Query: { user: (root, { id }) => findUser(id) }, // id: string
|
|
70
|
+
User: { name: (user) => user.name }, // user: what User maps to
|
|
71
|
+
DateTime: DateTimeResolver // a scalar this module defines
|
|
72
|
+
},
|
|
73
|
+
enums: { Role: { ADMIN: 1, GUEST: 2 } } // internal values, see below
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Unknown types and fields, other modules' fields and wrong return types are compile errors, reported at the field. `resolvers` is one map: inline like this, or what `loadResolvers` returns, which passes through unchecked since each file was typed where it was written. A scalar's implementation comes from the module whose SDL defines it, and only from that module.
|
|
78
|
+
|
|
79
|
+
## Features: splitting a large module
|
|
80
|
+
|
|
81
|
+
When a module grows, split it into features: subfolders that each own part of the module's schema.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
src/modules/sales/
|
|
85
|
+
├── index.ts # the module: module-wide middleware, loads its features
|
|
86
|
+
├── store.ts # shared code
|
|
87
|
+
├── typedefs/ # SDL the module itself owns (optional)
|
|
88
|
+
├── customers/ # feature "customers"
|
|
89
|
+
│ ├── index.ts
|
|
90
|
+
│ ├── typedefs/customers.gql
|
|
91
|
+
│ ├── query.ts mutation.ts middleware.ts
|
|
92
|
+
└── orders/ # feature "orders"
|
|
93
|
+
├── index.ts
|
|
94
|
+
├── typedefs/orders.gql
|
|
95
|
+
└── query.ts mutation.ts middleware.ts
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// sales/orders/index.ts
|
|
100
|
+
export const orders = feature.sales.orders({
|
|
101
|
+
typeDefs: await loadTypeDefs(new URL("typedefs/*.gql", import.meta.url)),
|
|
102
|
+
resolvers: await loadResolvers(
|
|
103
|
+
new URL("{query,mutation}.{js,ts}", import.meta.url)
|
|
104
|
+
),
|
|
105
|
+
middleware: mw.sales.orders({
|
|
106
|
+
Mutation: { cancelOrder: [requireRole("MANAGER")] }
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// sales/index.ts
|
|
111
|
+
export const salesModule = module.sales({
|
|
112
|
+
middleware: mw.sales({ Mutation: { "*": [journal()] } }), // every feature's fields
|
|
113
|
+
features: await loadFeatures(new URL("*/index.{js,ts}", import.meta.url))
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- **Discovery.** Codegen treats a direct subfolder of a module as a feature when the `typeDefs` glob finds files of its own there. With `**/typedefs/**/*.gql`, `sales/orders/typedefs/` makes `orders` a feature, while `sales/typedefs/` stays the module's own SDL. Features don't nest: deeper folders belong to their feature.
|
|
118
|
+
- **Isolation.** A feature owns the fields its SDL declares, like a module does, and only it can resolve them. To add a field to a sibling's type, `extend` it (`orders` adds `Customer.orders`).
|
|
119
|
+
- **Builders.** Each feature has its own typed builders: `query.sales.orders`, `mutation.sales.orders`, `resolvers.sales.orders.SalesOrder`, `mw.sales.orders`. The module's are still there (`query.sales` for fields the module itself declares).
|
|
120
|
+
- **Middleware nests.** For a feature's field, the chain runs app → module → feature. `mw.sales` reaches every feature's fields, and `mw.sales.orders` only orders'.
|
|
121
|
+
- **Names.** A feature folder can't share its name with a type the module declares, or with a function property (`name`, `length`, …), because both sit side by side in the builders. Codegen tells you if one does.
|
|
122
|
+
|
|
123
|
+
## Application
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { mw, olio } from "@nxara/olio";
|
|
127
|
+
|
|
128
|
+
const middleware = mw({
|
|
129
|
+
"*": { "*": [requestLogger()] },
|
|
130
|
+
Mutation: { "*": [requireAuth()] }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const app = olio({
|
|
134
|
+
middleware,
|
|
135
|
+
modules: await loadModules(
|
|
136
|
+
new URL("modules/*/index.{js,ts}", import.meta.url)
|
|
137
|
+
), // from @nxara/olio-tools
|
|
138
|
+
directives // the same list codegen uses; required once codegen had some
|
|
139
|
+
});
|
|
140
|
+
// app.schema → any GraphQL server (graphql-http, Yoga, Apollo, …)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
You can also list modules yourself: `modules: [usersModule, authModule]`.
|
|
144
|
+
|
|
145
|
+
`olio()` composes the modules' SDL through `@nxara/olio-schema` and checks the result before returning:
|
|
146
|
+
|
|
147
|
+
- **Root types come for free.** Modules `extend type Query` (or `Mutation`); nobody needs a placeholder `type Query { _empty: String }`. That also means a module that only extends root types can be tested alone: `olio({ modules: [usersModule] })`. One that uses another module's types takes that module too: `olio({ modules: [catalog, reviews] })`.
|
|
148
|
+
- **A module resolves only what it declares.** It owns the fields in its own definitions and `extend`s, and the defining module owns `__isTypeOf` / `__resolveType`. Resolving another module's field throws: `Module "posts": "User.name" is declared by module "users"; only that module can resolve it`. With codegen, the same message is the compile error, on the field: `Type '(row) => …' is not assignable to type '"User.name is declared by module users; only that module can resolve it"'`.
|
|
149
|
+
- **Errors point at files.** SDL and schema errors are reported as `file:line:column: message`.
|
|
150
|
+
|
|
151
|
+
## Middleware
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const requireAuth =
|
|
155
|
+
(): MiddlewareFn =>
|
|
156
|
+
({ root, args, ctx, info }, next) => {
|
|
157
|
+
if (!ctx.userId) throw new Error("Unauthorized");
|
|
158
|
+
return next();
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Written inline under a field key, in `mw.<id>()` or a module's `middleware`, a middleware gets that field's types: `root` is its parent (the mapper, if any) and `args` its arguments, so `mw.users({ User: { email: [({ root }, next) => …] } })` sees the `User` row. Root types have `root: undefined`, and `"*"` keys stay loose since they span fields. A middleware written on its own, like `requireAuth`, is typed `MiddlewareFn`: `root: unknown` and `args: Record<string, unknown>`, so it fits any key.
|
|
163
|
+
|
|
164
|
+
Middleware is nested, `{ Type: { field | "*": [...] } }`. For each resolver the chain runs top-down:
|
|
165
|
+
|
|
166
|
+
1. app `*.*`
|
|
167
|
+
2. app `Type.*`
|
|
168
|
+
3. app `Type.field`
|
|
169
|
+
4. module `Type.*`
|
|
170
|
+
5. module `Type.field`
|
|
171
|
+
6. feature `Type.*` (for a feature's field)
|
|
172
|
+
7. feature `Type.field`
|
|
173
|
+
|
|
174
|
+
The application addresses root types only, under whatever names the schema gives them: with `schema { query: RootQuery }` you write `mw({ RootQuery: { … } })` and `export const RootQuery = query.users({ … })`. A module addresses the fields it and its features declare, including fields they add to other modules' types with `extend`; a feature, only its own.
|
|
175
|
+
|
|
176
|
+
### Skipping middleware: `@bypass`
|
|
177
|
+
|
|
178
|
+
A field tells its middleware what to skip with `@bypass` in the SDL, and each middleware decides for itself:
|
|
179
|
+
|
|
180
|
+
```graphql
|
|
181
|
+
extend type Query {
|
|
182
|
+
me: Employee @bypass(keys: "auth")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# On a type, or an extend block: every field declared in that block.
|
|
186
|
+
extend type Mutation @bypass(keys: ["auth", "audit"]) {
|
|
187
|
+
login(email: String!, password: String!): LoginResult!
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const requireAuth =
|
|
193
|
+
(): MiddlewareFn =>
|
|
194
|
+
({ ctx, bypass }, next) => {
|
|
195
|
+
if (!ctx.userId && !bypass.has("auth")) throw unauthenticated();
|
|
196
|
+
return next();
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- **Per field, never per request.** `bypass` holds the keys of the field being resolved, so marking `login` doesn't relax anything else in the same operation.
|
|
201
|
+
- **Where it goes:** on a field of an object type, or on an object type (definition or `extend`), where it covers the fields declared in that block. A block in one module doesn't reach another module's fields on the same type. Anywhere else is an error with its location.
|
|
202
|
+
- **Out of the schema.** olio and codegen read and remove it while composing, so it isn't in the served schema, `schema.gql` or introspection. You don't need to declare it; `bypassDefinition` from `@nxara/olio-schema` is its SDL, for editor tooling. `@copy` doesn't carry it to copied fields.
|
|
203
|
+
- **Typed.** With codegen, `bypass` is a `ReadonlySet` of the keys used anywhere in the SDL (`BypassKey`), so `bypass.has("auht")` is a compile error. Untyped, it's a set of strings.
|
|
204
|
+
- **Nothing is dropped for you.** Other middleware, such as a logger, keeps running on a field marked `@bypass(keys: "auth")`.
|
|
205
|
+
|
|
206
|
+
Middleware wraps the resolvers modules define, and any field it names explicitly. `"*"` keys only reach fields with a resolver, but naming a field (`User: { email: [redact()] }`) wraps GraphQL's default resolver when the field has none, so the middleware always runs.
|
|
207
|
+
|
|
208
|
+
Every key is checked when `olio()` builds the schema, even without codegen. A key that matches no field, or an application key on a non-root type, throws.
|
|
209
|
+
|
|
210
|
+
## Subscriptions
|
|
211
|
+
|
|
212
|
+
A subscription field is a function that returns an async iterable of the field's values. Return the event source's own iterator, filtered at the source:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
export const Subscription = subscription.users({
|
|
216
|
+
userUpdated: (_root, { id }, ctx) =>
|
|
217
|
+
ctx.pubsub.subscribe("user:updated", (row) => row._id === id)
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
An async generator works too, for mapping events (`async *userUpdated() { for await (const row of …) yield … }`), with one catch: while it waits inside `for await`, it can only be ended at its next event. graphql-ws waits for that before running `onComplete` and `onClose`, so a client that leaves while nothing happens isn't cleaned up until something does. A source whose `return()` ends it at once (like [`apps/example-server/src/pubsub.ts`](../../apps/example-server/src/pubsub.ts)'s) doesn't have that problem.
|
|
222
|
+
|
|
223
|
+
- **Each yielded value is the field's value**, checked against the field type (with [mappers](#mappers) applied). Nested field resolvers run for every event.
|
|
224
|
+
- **Middleware runs once**, when a client subscribes, which is the place for auth and logging. Per-event logic belongs in the iterator.
|
|
225
|
+
- **Context:** subscription resolvers and `Subscription` middleware get `sub_ctx`, and `"*"` middleware can run on either kind: type it `MiddlewareFn<Context | SubscriptionContext>`. Without a separate `sub_ctx`, both are just `ctx`.
|
|
226
|
+
- **The event source is yours** (in-memory, Redis, …). [`apps/example-server/src/pubsub.ts`](../../apps/example-server/src/pubsub.ts) has a small typed in-memory one. To serve them, run graphql-ws or graphql-sse with `app.schema`: `@nxara/olio-http`'s README has the setup.
|
|
227
|
+
|
|
228
|
+
## Field extensions
|
|
229
|
+
|
|
230
|
+
A field's value can also be an object, `{ resolve?, extensions? }`, to attach `extensions` to the schema field. Tools read them from there: graphql-query-complexity's `complexity`, Grafast's plans, or your own middleware through `info.parentType.getFields()[info.fieldName].extensions`. Read back there, registered keys keep their types (`complexity` is a number, or a function of `{ args, childComplexity }`); graphql's index signature still lets any other key through as `unknown`.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
export const Query = query.inventory({
|
|
234
|
+
products: {
|
|
235
|
+
resolve: (_root, { filter }) => listProducts(filter),
|
|
236
|
+
extensions: {
|
|
237
|
+
complexity: ({ args, childComplexity }) =>
|
|
238
|
+
(args.filter?.pageSize ?? 20) * childComplexity
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
resolvers.users.User({ email: { extensions: { roles: ["ADMIN"] } } }); // no resolve: the default resolver stays
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Extensions are typed: register what they may hold, and anything else is a compile error. `Source` and `Args` are the field's parent and arguments, for extensions that are functions:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
declare module "@nxara/olio" {
|
|
249
|
+
interface FieldExtensions<Source, Args> {
|
|
250
|
+
// graphql-query-complexity's shape: the field's args and its selection's cost
|
|
251
|
+
complexity?:
|
|
252
|
+
| number
|
|
253
|
+
| ((estimate: { args: Args; childComplexity: number }) => number);
|
|
254
|
+
roles?: Role[];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Keys a library adds to graphql's own `GraphQLFieldExtensions` count too. Middleware still wraps `resolve`. Subscription fields take the same form, with the async generator as `resolve`. At runtime the object may hold only `resolve` and `extensions`. Without codegen, extensions are a plain `Record<string, unknown>`.
|
|
260
|
+
|
|
261
|
+
## Mappers
|
|
262
|
+
|
|
263
|
+
By default a resolver returns the generated type: a `User` resolver must return a full `User`. To return what you store instead and let field resolvers fill in the rest, register a mapper:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
declare module "@nxara/olio" {
|
|
267
|
+
interface Olio {
|
|
268
|
+
application: Application;
|
|
269
|
+
mappers: { User: UserRow };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Then every resolver that returns a `User` returns a `UserRow`. That holds through lists, nulls, unions, and the fields of other types, so `UserPage.data` takes rows too. `User` field resolvers also receive the row as `root`.
|
|
275
|
+
|
|
276
|
+
Mappers work for object types and interfaces, keyed by GraphQL type name:
|
|
277
|
+
|
|
278
|
+
- **An interface with a mapper** (`mappers: { Node: NodeRow }`): fields returning it return a `NodeRow`, and `__resolveType` receives one.
|
|
279
|
+
- **An interface without a mapper**: fields returning it return any implementer, each as its own mapper (`UserRow | Post`), because that is what graphql resolves. `__resolveType` receives the same union.
|
|
280
|
+
- **Unions** map member by member.
|
|
281
|
+
|
|
282
|
+
Enums with internal values are configured in codegen (`CustomEnums`) rather than here, because they also change argument and input types. See the codegen README. The values go in the owning module's (or feature's) `enums` option, never in `resolvers`; once `CustomEnums` registers an enum, `enums` is required, so they can't be forgotten. `resolvers.<id>.<Enum>({ ... })` types a map kept in its own file, and `loadEnums` from `@nxara/olio-tools` loads such files (unchecked, like `loadResolvers`). In an untyped app nothing knows values were expected, so a resolver returning an internal value no module mapped fails with `Enum "Role" cannot represent value: 1. No module gave "Role" internal values: ...`.
|
|
283
|
+
|
|
284
|
+
## Field types
|
|
285
|
+
|
|
286
|
+
Resolvers built with the builders are typed for you. For code outside them, such as services, loaders and tests, take the types from `generated.ts` and olio:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import type { Fields } from "@nxara/olio";
|
|
290
|
+
|
|
291
|
+
import type { LoginInput, Role, Scalars, User } from "./generated.js";
|
|
292
|
+
|
|
293
|
+
type UserArgs = Fields["Query"]["user"]["args"]; // { id: string }
|
|
294
|
+
type UserResult = Fields["Query"]["user"]["result"]; // User | null (UserRow | null with mappers)
|
|
295
|
+
type NameArgs = Fields["User"]["name"]["args"]; // { trim?: boolean | null }
|
|
296
|
+
type When = Scalars["DateTime"]["input"]; // a scalar, by direction
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
- Object, input, enum, union and interface types are exported by name from `generated.ts`.
|
|
300
|
+
- `Fields` covers every field a module declares, whichever module that is. It's plain indexed access, so your editor completes the type, then the field, then `args` / `result`.
|
|
301
|
+
- `result` is what the resolver returns, so with [mappers](#mappers) it's the mapped type.
|
|
302
|
+
- An argument with an SDL default (`limit: Int = 10`) is a required key typed `Maybe<T>`: graphql fills in the default before the resolver runs, but a client can still send `null`. A service called from elsewhere takes its own parameter type, e.g. `{ term: string; limit?: number | null }`.
|
|
303
|
+
|
|
304
|
+
## Without codegen or tools
|
|
305
|
+
|
|
306
|
+
Olio needs neither `@nxara/olio-codegen` nor `@nxara/olio-tools`. Write SDL as strings, resolvers as plain maps keyed by type, and build modules with `createModule` / `createFeature`. Everything works the same at runtime (middleware layers, features, enum values, subscriptions), only with looser types:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { createFeature, createModule, mw, olio } from "@nxara/olio";
|
|
310
|
+
|
|
311
|
+
const users = createModule("users", {
|
|
312
|
+
typeDefs:
|
|
313
|
+
"type User { id: ID! name: String! } extend type Query { user(id: ID!): User }",
|
|
314
|
+
// root: unknown, args: Record<string, unknown>, ctx: your registered ctx
|
|
315
|
+
resolvers: {
|
|
316
|
+
Query: { user: (_root, args) => ({ id: String(args.id), name: "Ada" }) }
|
|
317
|
+
},
|
|
318
|
+
middleware: { Query: { "*": [audit()] } },
|
|
319
|
+
features: [
|
|
320
|
+
createFeature("users", "admin", {
|
|
321
|
+
typeDefs: "extend type Mutation { ban(id: ID!): Boolean! }",
|
|
322
|
+
resolvers: { Mutation: { ban: () => true } }
|
|
323
|
+
})
|
|
324
|
+
]
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
export const app = olio({
|
|
328
|
+
middleware: mw({ Query: { "*": [requireAuth()] } }),
|
|
329
|
+
modules: [users]
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
- Use `createModule(id, config)` and `createFeature(module, id, config)`. Without an `Application` there are no known ids, so `module.<id>`, `query.<id>` and friends have nothing to check against.
|
|
334
|
+
- Key resolver maps by type: `{ Query: { user } }`. The builders (`query.users({ user })`) return the _inner_ map, so wrap them the same way: `{ Query: query.users({ user }) }`. `loadResolvers` does this for you, using export names.
|
|
335
|
+
- You can still register `ctx` (and `sub_ctx`) on their own, so middleware and resolvers get a typed context.
|
|
336
|
+
- To get the fully typed builders without running codegen, write the `Application` type by hand. It's a plain type: per module (and `"module/feature"`), each type's `{ type; fields; args? }`, root types with `root: "query" | "mutation" | "subscription"`, abstract types with `resolveType`, enums with `fields: never; enum: "A" | "B"`. Codegen's README shows the format.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FeatureConfig, FeatureId, FeatureMiddleware, Middleware, ModuleConfig, ModuleId, ModuleMiddleware, ModuleTypeName, MutationResolvers, OlioFeature, OlioModule, QueryResolvers, SubscriptionResolvers, TypeResolvers } from "./types.js";
|
|
2
|
+
type Builder<T> = (value: T) => T;
|
|
3
|
+
export declare const module: { [M in ModuleId]: (config: ModuleConfig<M>) => OlioModule; };
|
|
4
|
+
export declare const feature: { [M in ModuleId]: { [F in FeatureId<M>]: (config: FeatureConfig<M, F>) => OlioFeature; }; };
|
|
5
|
+
export declare const mw: Builder<Middleware> & { [M in ModuleId]: Builder<ModuleMiddleware<M>> & { [F in FeatureId<M>]: Builder<FeatureMiddleware<M, F>>; }; };
|
|
6
|
+
export declare const query: { [M in ModuleId]: Builder<QueryResolvers<M>> & { [F in FeatureId<M>]: Builder<QueryResolvers<`${M}/${F}`>>; }; };
|
|
7
|
+
export declare const mutation: { [M in ModuleId]: Builder<MutationResolvers<M>> & { [F in FeatureId<M>]: Builder<MutationResolvers<`${M}/${F}`>>; }; };
|
|
8
|
+
export declare const subscription: { [M in ModuleId]: Builder<SubscriptionResolvers<M>> & { [F in FeatureId<M>]: Builder<SubscriptionResolvers<`${M}/${F}`>>; }; };
|
|
9
|
+
type TypeBuilders<O extends string> = {
|
|
10
|
+
[T in ModuleTypeName<O>]: Builder<TypeResolvers<O, T>>;
|
|
11
|
+
};
|
|
12
|
+
export declare const resolvers: { [M in ModuleId]: TypeBuilders<M> & { [F in FeatureId<M>]: TypeBuilders<`${M}/${F}`>; }; };
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=builders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,cAAc,EACd,qBAAqB,EACrB,aAAa,EACd,MAAM,YAAY,CAAC;AAkBpB,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;AAElC,eAAO,MAAM,MAAM,EAGd,GACF,CAAC,IAAI,QAAQ,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,UAAU,GACzD,CAAC;AAEF,eAAO,MAAM,OAAO,EAKf,GACF,CAAC,IAAI,QAAQ,GAAG,GACd,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,WAAW,GAClE,GACF,CAAC;AAEF,eAAO,MAAM,EAAE,EAEE,OAAO,CAAC,UAAU,CAAC,GAAG,GACpC,CAAC,IAAI,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,GAC7C,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GACtD,GACF,CAAC;AAEF,eAAO,MAAM,KAAK,EAAyC,GACxD,CAAC,IAAI,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,GAAG,GAC3C,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GAC1D,GACF,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAyC,GAC3D,CAAC,IAAI,QAAQ,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,GAAG,GAC9C,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GAC7D,GACF,CAAC;AAEF,eAAO,MAAM,YAAY,EAAyC,GAC/D,CAAC,IAAI,QAAQ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,GAAG,GAClD,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GACjE,GACF,CAAC;AAEF,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,IAAI;KACnC,CAAC,IAAI,cAAc,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CACvD,CAAC;AAEF,eAAO,MAAM,SAAS,EAEN,GACb,CAAC,IAAI,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,GAChC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAC/C,GACF,CAAC"}
|
package/dist/builders.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFeature, createModule } from "./olio.js";
|
|
2
|
+
function perModule(make) {
|
|
3
|
+
return new Proxy({}, {
|
|
4
|
+
get: (_target, id) => make(String(id))
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
const identity = (value) => value;
|
|
8
|
+
const nested = new Proxy(identity, { get: () => identity });
|
|
9
|
+
export const module = perModule((id) => (config) => createModule(id, config));
|
|
10
|
+
export const feature = perModule((m) => perModule((f) => (config) => createFeature(m, f, config)));
|
|
11
|
+
export const mw = new Proxy(identity, {
|
|
12
|
+
get: () => nested
|
|
13
|
+
});
|
|
14
|
+
export const query = perModule(() => nested);
|
|
15
|
+
export const mutation = perModule(() => nested);
|
|
16
|
+
export const subscription = perModule(() => nested);
|
|
17
|
+
export const resolvers = perModule(() => perModule(() => nested));
|
|
18
|
+
//# sourceMappingURL=builders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builders.js","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAsBxD,SAAS,SAAS,CAAI,IAAuB;IAC3C,OAAO,IAAI,KAAK,CAAC,EAAuB,EAAE;QACxC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;KACvC,CAAC,CAAC;AACL,CAAC;AAGD,MAAM,QAAQ,GAAG,CAAI,KAAQ,EAAK,EAAE,CAAC,KAAK,CAAC;AAG3C,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;AAI5D,MAAM,CAAC,MAAM,MAAM,GAAG,SAAS,CAC7B,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAA8B,EAAE,EAAE,CACzC,YAAY,CAAC,EAAc,EAAE,MAAM,CAAC,CAGvC,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CACrC,SAAS,CACP,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAuC,EAAE,EAAE,CACjD,aAAa,CAAC,CAAa,EAAE,CAAC,EAAE,MAAM,CAAC,CAC1C,CAKF,CAAC;AAEF,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,QAAQ,EAAE;IACpC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM;CAClB,CAIA,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAI1C,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAI7C,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAIjD,CAAC;AAMF,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,EAAE,CACtC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAKxB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { feature, module, mutation, mw, query, resolvers, subscription } from "./builders.js";
|
|
2
|
+
export { createFeature, createModule, olio } from "./olio.js";
|
|
3
|
+
export type { OlioApp, OlioConfig } from "./olio.js";
|
|
4
|
+
export type { Context, FeatureConfig, BypassKey, DirectiveName, FeatureId, FeatureMiddleware, FieldExtensions, FieldResolver, Fields, LoadedEnums, LoadedResolvers, Middleware, MiddlewareFn, MiddlewareMap, MiddlewareParams, ModuleConfig, ModuleId, ModuleMiddleware, ModuleTypeName, MutationResolvers, Olio, OlioFeature, OlioModule, OwnerResolvers, QueryResolvers, ResolverMap, SubscriptionContext, SubscriptionResolvers, TypeResolvers, TypeSource } from "./types.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,MAAM,EACN,QAAQ,EACR,EAAE,EACF,KAAK,EACL,SAAS,EACT,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC9D,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACrD,YAAY,EACV,OAAO,EACP,aAAa,EACb,SAAS,EACT,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,MAAM,EACN,WAAW,EACX,eAAe,EACf,UAAU,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,IAAI,EACJ,WAAW,EACX,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,aAAa,EACb,UAAU,EACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,MAAM,EACN,QAAQ,EACR,EAAE,EACF,KAAK,EACL,SAAS,EACT,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/olio.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type SchemaDirective } from "@nxara/olio-schema";
|
|
2
|
+
import { type GraphQLSchema } from "graphql";
|
|
3
|
+
import type { FeatureConfig, DirectiveName, Middleware, ModuleConfig, ModuleId, OlioFeature, OlioModule } from "./types.js";
|
|
4
|
+
type DirectivesOption = [DirectiveName] extends [never] ? {
|
|
5
|
+
directives?: readonly SchemaDirective[];
|
|
6
|
+
} : {
|
|
7
|
+
directives: readonly SchemaDirective<DirectiveName>[];
|
|
8
|
+
};
|
|
9
|
+
export type OlioConfig = {
|
|
10
|
+
modules: readonly OlioModule[];
|
|
11
|
+
middleware?: Middleware;
|
|
12
|
+
} & DirectivesOption;
|
|
13
|
+
export interface OlioApp {
|
|
14
|
+
readonly schema: GraphQLSchema;
|
|
15
|
+
readonly modules: readonly OlioModule[];
|
|
16
|
+
}
|
|
17
|
+
export declare function createModule<M extends ModuleId>(id: M, config: ModuleConfig<M>): OlioModule;
|
|
18
|
+
export declare function createFeature<M extends ModuleId, F extends string>(module: M, id: F, config: FeatureConfig<M, F>): OlioFeature;
|
|
19
|
+
export declare function olio(config: OlioConfig): OlioApp;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=olio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"olio.d.ts","sourceRoot":"","sources":["../src/olio.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAqBL,KAAK,aAAa,EACnB,MAAM,SAAS,CAAC;AAMjB,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EAEb,UAAU,EAIV,YAAY,EACZ,QAAQ,EAER,WAAW,EACX,UAAU,EAGX,MAAM,YAAY,CAAC;AAIpB,KAAK,gBAAgB,GAAG,CAAC,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC,GACnD;IAEE,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;CACzC,GACD;IAEE,UAAU,EAAE,SAAS,eAAe,CAAC,aAAa,CAAC,EAAE,CAAC;CACvD,CAAC;AAEN,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,gBAAgB,CAAC;AAErB,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;CACzC;AAsED,wBAAgB,YAAY,CAAC,CAAC,SAAS,QAAQ,EAC7C,EAAE,EAAE,CAAC,EACL,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,UAAU,CAUZ;AAED,wBAAgB,aAAa,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,MAAM,EAChE,MAAM,EAAE,CAAC,EACT,EAAE,EAAE,CAAC,EACL,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,GAC1B,WAAW,CAUb;AAmnBD,wBAAgB,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAoBhD"}
|