vla 0.1.5 โ†’ 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.
Files changed (2) hide show
  1. package/README.md +36 -381
  2. package/package.json +3 -2
package/README.md CHANGED
@@ -1,426 +1,81 @@
1
1
  <p align="center">
2
2
  <img src=".github/logo-large.png" width="400px" align="center" alt="Vla Logo" />
3
- <h1 align="center">Vla โ€“ Smooth TypeScript Backends</h1>
3
+ <h1 align="center">Vla</h1>
4
+ <h3 align="center"><em>Makes TypeScript Backends sooo smooth</em></h3>
4
5
  <p align="center">
5
6
  Vla is the missing backend layer for scalable TypeScript apps that integrates into any framework and existing codebases.
6
7
  </p>
7
8
  </p>
8
9
  <br/>
9
10
 
10
- ๐Ÿšง WIP, under development. Exports and names will very likely change often.
11
+ > _Vla is currently in beta._
11
12
 
12
- - Dependency Injection without decorators and without reflection
13
- - Works in any server-side framework
14
- - Write code that's easy to test without module mocks all over the place
15
- - Structures code into modules, layers and interfaces
16
- - Familiar patterns: Actions, Services, Repos, Facades
17
- - Ensures that Facades are used for cross-module dependencies to prevent messy code dependencies
18
- - Tree shakeable
19
- - Memoziation for Repos
20
- - request-based context with AsyncLocalStorage
21
- - first-class context injection
22
- - ๐Ÿ—๏ธ auto tracing
23
- - ๐Ÿ—๏ธ input validation
24
- - ๐Ÿ—๏ธ error handling
25
- - ...
13
+ ## What is Vla?
26
14
 
27
- ## Install
28
-
29
- ```console
30
- npm install vla
31
- ```
32
-
33
- ## Why?
34
-
35
- 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.
36
-
37
- ## 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.
38
16
 
39
17
  ```ts
40
- import { Kernel, Vla } from "vla"
41
-
42
- // Create a Users module.
43
- // Modules are optional and helpful for larger apps.
44
- // Smaller apps might only need a single module.
45
- // You can use the provided `Vla.Action`, `Vla.Service` etc as shortcuts
46
- // instead of creating your own module
47
- const Users = Vla.createModule("Users")
48
-
49
- class ShowUserSettings extends Users.Action {
50
- users = this.inject(UserService)
18
+ class ShowUserProfile extends Vla.Action {
19
+ service = this.inject(UserService)
51
20
 
52
21
  async handle(userId: string) {
53
- const settings = await this.users.getSettings(userId)
54
- return {
55
- timezone: settings.timezone,
56
- hasSubscription: settings.hasSubscription
57
- }
22
+ return this.service.getProfile(userId)
58
23
  }
59
24
  }
60
25
 
61
- class UserService extends Users.Service {
26
+ class UserService extends Vla.Service {
62
27
  repo = this.inject(UserRepo)
63
28
  billing = this.inject(BillingFacade)
64
- session = this.inject(SessionFacade)
65
29
 
66
- async getSettings(userId: string) {
67
- await canViewProfile(userId)
68
-
69
- const profile = await this.repo.findById(userId)
30
+ async getProfile(userId: string) {
31
+ const user = await this.repo.findById(userId)
70
32
  const hasSubscription = await this.billing.hasSubscription(userId)
71
33
 
72
- return {
73
- ...profile,
74
- hasSubscription
75
- }
76
- }
77
-
78
- private async canViewProfile(userId: string) {
79
- const isSameUser = this.ctx.currentUser.id !== userId
80
- if (!isSameUser) throw new Forbidden()
81
-
82
- // repo method calls are memoized
83
- const profile = await this.repo.findById(userId)
84
- const currentUser = await this.session.currentUser()
85
- const isTeamAdmin =
86
- currentUser.role === "admin" &&
87
- currentUser.teamId === profile.teamId
88
-
89
- if (!isTeamAdmin) throw new Forbidden()
34
+ return { ...user, hasSubscription }
90
35
  }
91
36
  }
92
37
 
93
- class SessionFacade extends Users.Service {
94
- ctx = this.inject(ReqContext)
95
- repo = this.inject(UserRepo)
96
-
97
- async currentUser() {
98
- const currentUserId = cookies.userId
99
- const user = await this.repo.findById(currentUserId)
100
- return user
101
- }
102
- }
103
-
104
- class UserRepo extends Users.Repo {
38
+ class UserRepo extends Vla.Repo {
105
39
  db = this.inject(Database)
106
40
 
41
+ // Built-in memoization per request
107
42
  findById = this.memo((id: string) => {
108
- // this method is memoized per request.
109
- // memoized methods can be called like any normal method, but
110
- // if it's called multiple times with the same args, it's only
111
- // executed once and the result is cached
112
43
  return this.db.users.find({ id })
113
44
  })
114
-
115
- async create(data: UserValues) {
116
- const createdUser = await this.db.users.create({ data })
117
-
118
- this.findById.prime(createUser.id).value(createdUser)
119
- // memoized methods support multiple utilities:
120
- // - .fresh(args) to skip memoized cache and execute the function again
121
- // - .prime(args).value({ ... }) to set a cached value
122
- // - .preload(args) to run the method in the background and preload the cache
123
- // - .bust(args) to bust the cache for the provided args
124
- // - .bustAll() to bust the cache for all args
125
-
126
- return createdUser
127
- }
128
- }
129
-
130
- class Database extends Vla.Resource {
131
- static readonly unwrap = "db"
132
- // Unwraps a property when injecting the resource:
133
- // When another class injects the database with `this.inject(Database)`,
134
- // the `unwrap` will cause that it doesn't return the instance of the
135
- // Database class, but the `db` property of it.
136
- // This prevents that you have to write stuff like `this.db.db.find()`
137
-
138
- // `Resource` classes are singletons,
139
- // so the client this will only be initialized once
140
- db = new DbClient()
141
- }
142
-
143
- // Billing
144
- const Billing = Vla.createModule("Billing")
145
-
146
- class BillingFacade extends Billing.Facade {
147
- repo = this.inject(BillingRepo)
148
-
149
- async hasSubscription(userId: string) {
150
- const subscription = await this.repo.findSubscriptionByUser(userId)
151
- return Boolean(subscription)
152
- }
153
- }
154
-
155
- class BillingRepo extends BillingModule.Repo {
156
- async findSubscriptionByUser(userId: string) {
157
- return db.subscriptions.find({ userId })
158
- }
159
- }
160
-
161
- // Supports injecting context
162
- const ReqContext = Vla.createContext<{ cookies: Record<string, unknown> }>()
163
-
164
- const kernel = new Kernel()
165
- Vla.setGlobalInvokeKernel(kernel) // define as global instance
166
-
167
- // just an example. you should use a scoped context instead (see below)
168
- kernel.context(ReqContext, { cookies: req.cookies })
169
-
170
- const settings = await ShowUserSettings.invoke(userId)
171
- // -> { timezone: 'GMT+1', hasSubscription: true }
172
- ```
173
-
174
- ### React usage
175
-
176
- ```tsx
177
- import { cache } from 'react'
178
- import { Vla } from 'vla'
179
- import { kernel } from '@/data/kernel'
180
-
181
- const kernel = new Kernel()
182
-
183
- // React's cache() will return a new scoped kernel for each request
184
- Vla.setInvokeKernelProvider(cache(() => {
185
- return kernel
186
- .scoped()
187
- .context(ReqContext, {
188
- // you can either use await cookies() here or await the promise in Vla
189
- cookies: cookies()
190
- })
191
- }))
192
-
193
- async function Layout() {
194
- const settings = await ShowUserSettings.invoke(userId)
195
-
196
- return <div><Page /></div>
197
- }
198
-
199
- async function Page() {
200
- const settings = await ShowUserSettings.invoke(userId)
201
- // it will not query the db twice. it will use the memoized db query
202
-
203
- return (
204
- <ul>
205
- <li>Timezone: {settings.timezone}</li>
206
- <li>Subscriber: {settings.hasSubscription ? "yes" : "no"}</li>
207
- </ul>
208
- )
209
45
  }
210
46
  ```
211
47
 
212
- ### In any middleware-based app
48
+ ## Features
213
49
 
214
- e.g. sveltekit
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
215
58
 
216
- ```ts
217
- import { Vla } from "vla"
218
- import type { Handle } from "@sveltejs/kit"
219
- import { kernel } from '@/data/kernel'
59
+ ## Why Vla?
220
60
 
221
- export const handle: Handle = async ({ event, resolve }) => {
222
- return Vla.withKernel(kernel.scoped(), () => resolve(event))
223
- }
61
+ Fullstack TypeScript frameworks excel at the frontend: routing, rendering, server actions, but leave the backend data layer unstructured. Without conventions, codebases become messy:
224
62
 
225
- import { kernel } from '@/data/kernel';
226
- import type { PageServerLoad } from './$types';
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
227
66
 
228
- export const load: PageServerLoad = async ({ params }) => {
229
- return {
230
- settings: await ShowUserSettings.invoke(userId)
231
- }
232
- }
233
- ```
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.
234
68
 
235
- e.g. express
69
+ ## Installation
236
70
 
237
- ```ts
238
- import { Vla } from "vla"
239
- import { kernel } from '@/data/kernel'
240
-
241
- const app = express()
242
-
243
- express.use((req, res, next) => {
244
- const scope = kernel.scoped().context(CustomContext, { req })
245
- return Vla.withKernel(scope, () => next())
246
- })
247
-
248
- app.get("/users/:id", async (req, res) => {
249
- const settings = await ShowUserSettings.invoke(req.params.id)
250
- res.json({ data: settings })
251
- })
252
- ```
253
-
254
- ### Testing
255
-
256
- ```ts
257
- test("returns some user settings", async () => {
258
- const kernel = new Kernel()
259
-
260
- // mocks for database calls
261
- kernel.bind(
262
- UserRepo,
263
- vi.fn(
264
- class {
265
- findById = vi.fn().mockResolvedValue({ timezone: "faked" })
266
- },
267
- ),
268
- )
269
-
270
- // mocks for cross-module dependencies
271
- kernel.bind(
272
- BillingFacade,
273
- vi.fn(
274
- class {
275
- hasSubscription = vi.fn().mockResolvedValue(true)
276
- },
277
- ),
278
- )
279
-
280
- // mock a context
281
- kernel.bindValue(
282
- ReqContext,
283
- { cookies: { currentUserId: 1 }}
284
- )
285
-
286
- await expect(ShowUserSettings.withKernel(kernel).invoke("1")).resolves.toEqual({
287
- timezone: "faked",
288
- hasSubscription: true
289
- })
290
- })
291
- ```
292
-
293
- ## Docs
294
-
295
- There aren't any docs yet. This section is just jotting down some notes for myself.
296
-
297
- ### When to use modules?
298
-
299
- 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.
300
-
301
- 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.
302
-
303
- ### What's a facade, when to use it?
304
-
305
- Facades are meant as the internal public API to a module, for other modules.
306
-
307
- 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.
308
-
309
- Vla prevents that any module can deeply inject any arbitrary class from another module. It only allows injecting Resources and Facades from other modules.
310
-
311
- ### How should I structure files and folders?
312
-
313
- Roughly follow how Vla structures your code:
314
-
315
- - Use separate folders for separate modules
316
- - Create a separate file for each Service, Repo, Facade, Action
317
- - 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.
318
-
319
- "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.
320
-
321
- ### Class scopes
322
-
323
- 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.
324
-
325
- There are 3 different scopes:
326
-
327
- - `transient` creates a new instance every time, it does not use a cached instance
328
- - `invoke` creates a new instance per request handler and reuses this cached instance within the request.
329
- - `singleton` creates a new instance only once and caches it forever
330
-
331
- Example:
332
-
333
- ```ts
334
- class DatabasePool extends MyModule.Resource {
335
- // Resources are Singletons.
336
- // Singletons are cached globally
337
- private dbPool: DbPool | null = null
338
-
339
- get dbPool() {
340
- return this.dbPool || new DbPool()
341
- }
342
- }
343
-
344
- class Repo extends MyModule.Repo {
345
- // repos are cached per request ("invoke" scope),
346
- // so this map will be stateful for the current request scope
347
- private cache = Map<string, User>()
348
-
349
- dbPool = this.inject(DatabasePool)
350
-
351
- async findById(id: string) {
352
- const cached = this.cache.get(id)
353
- if (cached) return cached
354
-
355
- const user = await dbPool.user.find({ id })
356
- this.cache.set(id, user)
357
- return user
358
- }
359
- }
360
-
361
- class ServiceA extends MyModule.Service {
362
- repo = this.inject(Repo)
363
- // This repo instance is exactly the same instance as in ServiceB,
364
- // so they both share the same `.cache`
365
-
366
- async doSomething() {
367
- await this.repo.findById("1")
368
- }
369
- }
370
-
371
- class ServiceB extends MyModule.Service {
372
- repo = this.inject(Repo)
373
- // This repo instance is exactly the same instance as in ServiceA,
374
- // so they both share the same `.cache`
375
-
376
- async doOtherThing() {
377
- await this.repo.findById("1")
378
- }
379
- }
380
- ```
381
-
382
- In this example:
383
- - The `DatabasePool` will only be created once and will be reused forever
384
- - 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.
385
- - Both `ServiceA` and `ServiceB` will share the exact same `Repo` instance during a request.
386
-
387
- ### Overriding class scopes
388
-
389
- ```ts
390
- class FooService extends MyModule.Service {
391
- static scope = FooClass.ScopeTransient
392
- // Services have a "invoke" scope by default. This overrides it to be
393
- // "transient" instead. Transient classes get created separately for
394
- // each usage and aren't cached at all.
395
- }
396
- ```
397
-
398
- ### Defining scope when injecting a class
399
-
400
- Example:
401
-
402
- ```ts
403
- class FooRepo extends MyModule.Repo {
404
- async foo() {}
405
- }
406
-
407
- class FooService extends MyModule.Service {
408
- repo = this.inject(FooRepo, "transient")
409
- }
71
+ ```bash
72
+ npm install vla
410
73
  ```
411
74
 
412
- 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.
413
-
414
- 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.
415
-
416
- ### Base classes
75
+ ## [Documentation](https://vla.run/guides/installation/)
417
76
 
418
- 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.
419
78
 
420
- - `Action` for server-side entry points (scope: `transient`)
421
- - `Service` for reusable units of code (scope: `invoke`)
422
- - `Repo` for data access and external adapters (scope: `invoke`)
423
- - `Resource` for long-lived infrastructure clients such as database pools (scope: `singleton`)
424
- - `Facade` for the interface of a module for cross-module access (scope: `transient`)
79
+ ## License
425
80
 
426
- 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vla",
3
3
  "type": "module",
4
- "version": "0.1.5",
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
  }