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.
- package/README.md +36 -381
- 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
|
|
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
|
-
|
|
11
|
+
> _Vla is currently in beta._
|
|
11
12
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
67
|
-
await
|
|
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
|
|
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
|
-
|
|
48
|
+
## Features
|
|
213
49
|
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
import { Vla } from "vla"
|
|
218
|
-
import type { Handle } from "@sveltejs/kit"
|
|
219
|
-
import { kernel } from '@/data/kernel'
|
|
59
|
+
## Why Vla?
|
|
220
60
|
|
|
221
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
## Installation
|
|
236
70
|
|
|
237
|
-
```
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
Check out [vla.run](https://vla.run) for guides, references and framework integrations.
|
|
419
78
|
|
|
420
|
-
|
|
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
|
-
|
|
81
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vla",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
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
|
}
|