vla 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,423 +1,81 @@
1
- # Vla
1
+ <p align="center">
2
+ <img src=".github/logo-large.png" width="400px" align="center" alt="Vla Logo" />
3
+ <h1 align="center">Vla</h1>
4
+ <h3 align="center"><em>Makes TypeScript Backends sooo smooth</em></h3>
5
+ <p align="center">
6
+ Vla is the missing backend layer for scalable TypeScript apps that integrates into any framework and existing codebases.
7
+ </p>
8
+ </p>
9
+ <br/>
2
10
 
3
- _A smooth dutch dessert that goes with everything._
11
+ > _Vla is currently in beta._
4
12
 
5
- A TypeScript data layer kernel for backend- and fullstack apps. Compatible with whatever framework or library you're using.
13
+ ## What is Vla?
6
14
 
7
- 🚧 WIP, under development. Exports and names will very likely change often.
8
-
9
- - Dependency Injection without decorators and without reflection
10
- - Works in any server-side framework
11
- - Write code that's easy to test without module mocks all over the place
12
- - Structures code into modules, layers and interfaces
13
- - Familiar patterns: Actions, Services, Repos, Facades
14
- - Ensures that Facades are used for cross-module dependencies to prevent messy code dependencies
15
- - Tree shakeable
16
- - Memoziation for Repos
17
- - request-based context with AsyncLocalStorage
18
- - first-class context injection
19
- - 🏗️ auto tracing
20
- - 🏗️ input validation
21
- - 🏗️ error handling
22
- - ...
23
-
24
- ## Install
25
-
26
- ```console
27
- npm install vla
28
- ```
29
-
30
- ## Why?
31
-
32
- Many fullstack frameworks lack structure and conventions on the backend side (data layer), but they have lots of good structure and conventions on the frontend side (presentation layer). They are still great frameworks and they all have their own strengths. This is where Vla comes in. It aims to fill in the missing gap in the data layer, allowing you to write well-structured maintainable, scalable and testable code.
33
-
34
- ## Usage
15
+ Vla structures your backend code with clear layers and dependency injection, without decorators or reflection. It works alongside your framework (Next.js, SvelteKit, Express, etc.) to organize your data layer with familiar patterns: Actions, Services, Repos, and Resources.
35
16
 
36
17
  ```ts
37
- import { Kernel, Vla } from "vla"
38
-
39
- // Create a Users module.
40
- // Modules are optional and helpful for larger apps.
41
- // Smaller apps might only need a single module.
42
- // You can use the provided `Vla.Action`, `Vla.Service` etc as shortcuts
43
- // instead of creating your own module
44
- const Users = Vla.createModule("Users")
45
-
46
- class ShowUserSettings extends Users.Action {
47
- users = this.inject(UserService)
18
+ class ShowUserProfile extends Vla.Action {
19
+ service = this.inject(UserService)
48
20
 
49
21
  async handle(userId: string) {
50
- const settings = await this.users.getSettings(userId)
51
- return {
52
- timezone: settings.timezone,
53
- hasSubscription: settings.hasSubscription
54
- }
22
+ return this.service.getProfile(userId)
55
23
  }
56
24
  }
57
25
 
58
- class UserService extends Users.Service {
26
+ class UserService extends Vla.Service {
59
27
  repo = this.inject(UserRepo)
60
28
  billing = this.inject(BillingFacade)
61
- session = this.inject(SessionFacade)
62
29
 
63
- async getSettings(userId: string) {
64
- await canViewProfile(userId)
65
-
66
- const profile = await this.repo.findById(userId)
30
+ async getProfile(userId: string) {
31
+ const user = await this.repo.findById(userId)
67
32
  const hasSubscription = await this.billing.hasSubscription(userId)
68
33
 
69
- return {
70
- ...profile,
71
- hasSubscription
72
- }
73
- }
74
-
75
- private async canViewProfile(userId: string) {
76
- const isSameUser = this.ctx.currentUser.id !== userId
77
- if (!isSameUser) throw new Forbidden()
78
-
79
- // repo method calls are memoized
80
- const profile = await this.repo.findById(userId)
81
- const currentUser = await this.session.currentUser()
82
- const isTeamAdmin =
83
- currentUser.role === "admin" &&
84
- currentUser.teamId === profile.teamId
85
-
86
- if (!isTeamAdmin) throw new Forbidden()
34
+ return { ...user, hasSubscription }
87
35
  }
88
36
  }
89
37
 
90
- class SessionFacade extends Users.Service {
91
- ctx = this.inject(ReqContext)
92
- repo = this.inject(UserRepo)
93
-
94
- async currentUser() {
95
- const currentUserId = cookies.userId
96
- const user = await this.repo.findById(currentUserId)
97
- return user
98
- }
99
- }
100
-
101
- class UserRepo extends Users.Repo {
38
+ class UserRepo extends Vla.Repo {
102
39
  db = this.inject(Database)
103
40
 
41
+ // Built-in memoization per request
104
42
  findById = this.memo((id: string) => {
105
- // this method is memoized per request.
106
- // memoized methods can be called like any normal method, but
107
- // if it's called multiple times with the same args, it's only
108
- // executed once and the result is cached
109
43
  return this.db.users.find({ id })
110
44
  })
111
-
112
- async create(data: UserValues) {
113
- const createdUser = await this.db.users.create({ data })
114
-
115
- this.findById.prime(createUser.id).value(createdUser)
116
- // memoized methods support multiple utilities:
117
- // - .fresh(args) to skip memoized cache and execute the function again
118
- // - .prime(args).value({ ... }) to set a cached value
119
- // - .preload(args) to run the method in the background and preload the cache
120
- // - .bust(args) to bust the cache for the provided args
121
- // - .bustAll() to bust the cache for all args
122
-
123
- return createdUser
124
- }
125
- }
126
-
127
- class Database extends Vla.Resource {
128
- static override unwrap = "db"
129
- // Unwraps a property when injecting the resource:
130
- // When another class injects the database with `this.inject(Database)`,
131
- // the `unwrap` will cause that it doesn't return the instance of the
132
- // Database class, but the `db` property of it.
133
- // This prevents that you have to write stuff like `this.db.db.find()`
134
-
135
- // `Resource` classes are singletons,
136
- // so the client this will only be initialized once
137
- db = new DbClient()
138
- }
139
-
140
- // Billing
141
- const Billing = Vla.createModule("Billing")
142
-
143
- class BillingFacade extends Billing.Facade {
144
- repo = this.inject(BillingRepo)
145
-
146
- async hasSubscription(userId: string) {
147
- const subscription = await this.repo.findSubscriptionByUser(userId)
148
- return Boolean(subscription)
149
- }
150
- }
151
-
152
- class BillingRepo extends BillingModule.Repo {
153
- async findSubscriptionByUser(userId: string) {
154
- return db.subscriptions.find({ userId })
155
- }
156
45
  }
157
-
158
- // Supports injecting context
159
- const ReqContext = Vla.createContext<{ cookies: Record<string, unknown> }>()
160
-
161
- const kernel = new Kernel()
162
- Vla.setGlobalInvokeKernel(kernel) // define as global instance
163
-
164
- // just an example. you should use a scoped context instead (see below)
165
- kernel.context(ReqContext, { cookies: req.cookies })
166
-
167
- const settings = await ShowUserSettings.invoke(userId)
168
- // -> { timezone: 'GMT+1', hasSubscription: true }
169
46
  ```
170
47
 
171
- ### React usage
48
+ ## Features
172
49
 
173
- ```tsx
174
- import { cache } from 'react'
175
- import { Vla } from 'vla'
176
- import { kernel } from '@/data/kernel'
50
+ - **Framework Agnostic** – Works with Next.js, SvelteKit, Express, Koa, and any TypeScript framework
51
+ - **Clear Architecture** Actions, Services, Repos, Resources, and Facades for organized code
52
+ - **Clean Dependency Injection** - No decorators, no reflection, just `this.inject()`
53
+ - **Built-in Memoization** Automatic request-scoped caching for database queries
54
+ - **Easy Testing** – Test classes, not file paths—no more brittle module mocks
55
+ - **Module System** – Scale to large apps with domain-separated modules and Facades
56
+ - **Request Context** – First-class context injection with AsyncLocalStorage
57
+ - **Tree Shakeable** – Only bundle what you use
177
58
 
178
- const kernel = new Kernel()
179
-
180
- // React's cache() will return a new scoped kernel for each request
181
- Vla.setInvokeKernelProvider(cache(() => {
182
- return kernel
183
- .scoped()
184
- .context(ReqContext, {
185
- // you can either use await cookies() here or await the promise in Vla
186
- cookies: cookies()
187
- })
188
- }))
189
-
190
- async function Layout() {
191
- const settings = await ShowUserSettings.invoke(userId)
192
-
193
- return <div><Page /></div>
194
- }
195
-
196
- async function Page() {
197
- const settings = await ShowUserSettings.invoke(userId)
198
- // it will not query the db twice. it will use the memoized db query
199
-
200
- return (
201
- <ul>
202
- <li>Timezone: {settings.timezone}</li>
203
- <li>Subscriber: {settings.hasSubscription ? "yes" : "no"}</li>
204
- </ul>
205
- )
206
- }
207
- ```
59
+ ## Why Vla?
208
60
 
209
- ### In any middleware-based app
61
+ Fullstack TypeScript frameworks excel at the frontend: routing, rendering, server actions, but leave the backend data layer unstructured. Without conventions, codebases become messy:
210
62
 
211
- e.g. sveltekit
63
+ - Unclear separation between business logic and data access
64
+ - Testing requires module mocks that leak file paths into tests
65
+ - Code dependencies become tangled as the app grows
212
66
 
213
- ```ts
214
- import { Vla } from "vla"
215
- import type { Handle } from "@sveltejs/kit"
216
- import { kernel } from '@/data/kernel'
217
-
218
- export const handle: Handle = async ({ event, resolve }) => {
219
- return Vla.withKernel(kernel.scoped(), () => resolve(event))
220
- }
221
-
222
- import { kernel } from '@/data/kernel';
223
- import type { PageServerLoad } from './$types';
224
-
225
- export const load: PageServerLoad = async ({ params }) => {
226
- return {
227
- settings: await ShowUserSettings.invoke(userId)
228
- }
229
- }
230
- ```
231
-
232
- e.g. express
233
-
234
- ```ts
235
- import { Vla } from "vla"
236
- import { kernel } from '@/data/kernel'
237
-
238
- const app = express()
239
-
240
- express.use((req, res, next) => {
241
- const scope = kernel.scoped().context(CustomContext, { req })
242
- return Vla.withKernel(scope, () => next())
243
- })
244
-
245
- app.get("/users/:id", async (req, res) => {
246
- const settings = await ShowUserSettings.invoke(req.params.id)
247
- res.json({ data: settings })
248
- })
249
- ```
67
+ **Vla fills this gap.** It provides structure and conventions for your data layer without replacing your framework. It's a library, not a framework. No HTTP server, no build tools, just patterns that scale.
250
68
 
251
- ### Testing
69
+ ## Installation
252
70
 
253
- ```ts
254
- test("returns some user settings", async () => {
255
- const kernel = new Kernel()
256
-
257
- // mocks for database calls
258
- kernel.bind(
259
- UserRepo,
260
- vi.fn(
261
- class {
262
- findById = vi.fn().mockResolvedValue({ timezone: "faked" })
263
- },
264
- ),
265
- )
266
-
267
- // mocks for cross-module dependencies
268
- kernel.bind(
269
- BillingFacade,
270
- vi.fn(
271
- class {
272
- hasSubscription = vi.fn().mockResolvedValue(true)
273
- },
274
- ),
275
- )
276
-
277
- // mock a context
278
- kernel.bindValue(
279
- ReqContext,
280
- { cookies: { currentUserId: 1 }}
281
- )
282
-
283
- await expect(ShowUserSettings.withKernel(kernel).invoke("1")).resolves.toEqual({
284
- timezone: "faked",
285
- hasSubscription: true
286
- })
287
- })
288
- ```
289
-
290
- ## Docs
291
-
292
- There aren't any docs yet. This section is just jotting down some notes for myself.
293
-
294
- ### When to use modules?
295
-
296
- You don't need to create a module for each separate resource. Modules are meant for domain separation, not necessarily for resources. A module can have multiple services, repositories and even multiple facades to separate resources from each other.
297
-
298
- Smaller apps may just need a single module for the whole app. You can use `Vla.Action`, `Vla.Repo`, `Vla.Service` etc as a shortcut.
299
-
300
- ### What's a facade, when to use it?
301
-
302
- Facades are meant as the internal public API to a module, for other modules.
303
-
304
- When one module wants to use something from another module, it should do so by loading a facade, and not by deeply calling a service or repository of a module. Even though this adds a layer of indirection, it lets you better differentiate between internal and external concerns of a module, and prevents code dependencies from becoming messy. If you use facades excessively often, your domain modelling could be improved.
305
-
306
- Vla prevents that any module can deeply inject any arbitrary class from another module. It only allows injecting Resources and Facades from other modules.
307
-
308
- ### How should I structure files and folders?
309
-
310
- Roughly follow how Vla structures your code:
311
-
312
- - Use separate folders for separate modules
313
- - Create a separate file for each Service, Repo, Facade, Action
314
- - If you further want to group files inside a module, do it either by resource (`user`, `post`, etc) or by type (`services`, `repos`, `actions`, etc). Start by having a flat hierarchy with all files of a module in a single folder, and separate into multiple folders once you feel it grows too much.
315
-
316
- "How to structure code into files and folders" is often a question of how to manage code dependencies in a way that scales well. Vla's structure already manages your code dependencies, so it makes sense to align your files and folders on Vla's structure.
317
-
318
- ### Class scopes
319
-
320
- Vla uses dependency injection to load and use classes in other classes. This means that Vla creates class instances for you and caches them in different caches for different lifetimes. Whenever you call `inject()`, Vla checks the scope of the class and whether an instance is already cached.
321
-
322
- There are 3 different scopes:
323
-
324
- - `transient` creates a new instance every time, it does not use a cached instance
325
- - `invoke` creates a new instance per request handler and reuses this cached instance within the request.
326
- - `singleton` creates a new instance only once and caches it forever
327
-
328
- Example:
329
-
330
- ```ts
331
- class DatabasePool extends MyModule.Resource {
332
- // Resources are Singletons.
333
- // Singletons are cached globally
334
- private dbPool: DbPool | null = null
335
-
336
- get dbPool() {
337
- return this.dbPool || new DbPool()
338
- }
339
- }
340
-
341
- class Repo extends MyModule.Repo {
342
- // repos are cached per request ("invoke" scope),
343
- // so this map will be stateful for the current request scope
344
- private cache = Map<string, User>()
345
-
346
- dbPool = this.inject(DatabasePool)
347
-
348
- async findById(id: string) {
349
- const cached = this.cache.get(id)
350
- if (cached) return cached
351
-
352
- const user = await dbPool.user.find({ id })
353
- this.cache.set(id, user)
354
- return user
355
- }
356
- }
357
-
358
- class ServiceA extends MyModule.Service {
359
- repo = this.inject(Repo)
360
- // This repo instance is exactly the same instance as in ServiceB,
361
- // so they both share the same `.cache`
362
-
363
- async doSomething() {
364
- await this.repo.findById("1")
365
- }
366
- }
367
-
368
- class ServiceB extends MyModule.Service {
369
- repo = this.inject(Repo)
370
- // This repo instance is exactly the same instance as in ServiceA,
371
- // so they both share the same `.cache`
372
-
373
- async doOtherThing() {
374
- await this.repo.findById("1")
375
- }
376
- }
377
- ```
378
-
379
- In this example:
380
- - The `DatabasePool` will only be created once and will be reused forever
381
- - The `Repo` will be stateful for each request. A new instance will be created for each request, and reused for the lifetime of the request.
382
- - Both `ServiceA` and `ServiceB` will share the exact same `Repo` instance during a request.
383
-
384
- ### Overriding class scopes
385
-
386
- ```ts
387
- class FooService extends MyModule.Service {
388
- static scope = FooClass.ScopeTransient
389
- // Services have a "invoke" scope by default. This overrides it to be
390
- // "transient" instead. Transient classes get created separately for
391
- // each usage and aren't cached at all.
392
- }
393
- ```
394
-
395
- ### Defining scope when injecting a class
396
-
397
- Example:
398
-
399
- ```ts
400
- class FooRepo extends MyModule.Repo {
401
- async foo() {}
402
- }
403
-
404
- class FooService extends MyModule.Service {
405
- repo = this.inject(FooRepo, "transient")
406
- }
71
+ ```bash
72
+ npm install vla
407
73
  ```
408
74
 
409
- This will use a transient version of the FooRepo only inside the `FooService`. Meaning this service won't share the same cached version that other services might be using.
410
-
411
- It's not something you'll likely need to use often. But this shows how the scope defines how long the instance should be cached and shared.
412
-
413
- ### Base classes
75
+ ## [Documentation](https://vla.run/guides/installation/)
414
76
 
415
- Vla gives you multiple base classes with semantic names. You can use them as they are, or extend them for your own custom base classes.
77
+ Check out [vla.run](https://vla.run) for guides, references and framework integrations.
416
78
 
417
- - `Action` for server-side entry points (scope: `transient`)
418
- - `Service` for reusable units of code (scope: `invoke`)
419
- - `Repo` for data access and external adapters (scope: `invoke`)
420
- - `Resource` for long-lived infrastructure clients such as database pools (scope: `singleton`)
421
- - `Facade` for the interface of a module for cross-module access (scope: `transient`)
79
+ ## License
422
80
 
423
- Facades and Actions are similar in the way that they provide an interface into the module. The difference is that Actions are meant to be invoked from outside of a module, while Facades are meant to be called from inside of a module.
81
+ MIT
package/dist/index.d.mts CHANGED
@@ -86,16 +86,6 @@ declare abstract class BaseAction {
86
86
  };
87
87
  }
88
88
  //#endregion
89
- //#region src/kernel/scoped-als.d.ts
90
- declare function withKernel<T>(scopedKernel: Kernel, fn: () => T): T;
91
- //#endregion
92
- //#region src/kernel/scoped-global.d.ts
93
- declare function setGlobalInvokeKernel(kernel: Kernel): void;
94
- //#endregion
95
- //#region src/kernel/scoped-provider.d.ts
96
- type CurrentKernelFn = () => Kernel | Promise<Kernel>;
97
- declare function setInvokeKernelProvider(fn: CurrentKernelFn): void;
98
- //#endregion
99
89
  //#region src/concerns/devstable.d.ts
100
90
  type BaseCtor<T = object> = abstract new (...args: any[]) => T;
101
91
  declare function DevStable<TBase extends BaseCtor>(Base: TBase): ((abstract new (...args: any[]) => {
@@ -211,6 +201,16 @@ declare function createModule<const ModuleName extends string>(moduleName: Modul
211
201
  DevStable: typeof DevStable;
212
202
  };
213
203
  //#endregion
204
+ //#region src/kernel/scoped-als.d.ts
205
+ declare function withKernel<T>(scopedKernel: Kernel, fn: () => T): T;
206
+ //#endregion
207
+ //#region src/kernel/scoped-global.d.ts
208
+ declare function setGlobalInvokeKernel(kernel: Kernel): void;
209
+ //#endregion
210
+ //#region src/kernel/scoped-provider.d.ts
211
+ type CurrentKernelFn = () => Kernel | Promise<Kernel>;
212
+ declare function setInvokeKernelProvider(fn: CurrentKernelFn): void;
213
+ //#endregion
214
214
  //#region src/index.d.ts
215
215
  declare const Vla: {
216
216
  createModule: typeof createModule;
@@ -218,6 +218,7 @@ declare const Vla: {
218
218
  withKernel: typeof withKernel;
219
219
  setGlobalInvokeKernel: typeof setGlobalInvokeKernel;
220
220
  setInvokeKernelProvider: typeof setInvokeKernelProvider;
221
+ Kernel: typeof Kernel;
221
222
  Facade: (abstract new () => {
222
223
  inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
223
224
  readonly parentLayers: readonly Layer[];
package/dist/index.mjs CHANGED
@@ -413,7 +413,8 @@ const Vla = {
413
413
  createContext,
414
414
  withKernel,
415
415
  setGlobalInvokeKernel,
416
- setInvokeKernelProvider
416
+ setInvokeKernelProvider,
417
+ Kernel
417
418
  };
418
419
 
419
420
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vla",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "description": "Data layer kernel for backend- and fullstack apps",
6
6
  "author": "Timo Mämecke <hello@timo.wtf>",
7
7
  "license": "MIT",
@@ -41,5 +41,6 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "object-hash": "^3.0.0"
44
- }
44
+ },
45
+ "packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6"
45
46
  }