@venizia/ignis-atlas 0.1.0-13 → 0.1.0-15
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/dist/corpus/changelogs/2025-12-31-json-path-filtering-array-operators.md +3 -3
- package/dist/corpus/changelogs/2026-09-14-an-application-declares-no-artifacts.md +105 -0
- package/dist/corpus/changelogs/2026-09-14-asset-options-take-resolvers.md +59 -0
- package/dist/corpus/changelogs/2026-09-14-component-options-reach-binding.md +61 -0
- package/dist/corpus/changelogs/2026-09-14-download-route-singular.md +57 -0
- package/dist/corpus/changelogs/2026-09-14-inject-target-through-an-import-cycle.md +85 -0
- package/dist/corpus/changelogs/2026-09-14-metalink-table-is-yours.md +92 -0
- package/dist/corpus/changelogs/2026-09-14-stats-key-header-in-api-reference.md +44 -0
- package/dist/corpus/changelogs/2026-09-14-unresolved-binding-report.md +71 -0
- package/dist/corpus/changelogs/index.md +8 -0
- package/dist/corpus/releases.json +157 -0
- package/dist/corpus/symbols.json +270 -120
- package/dist/corpus/wiki/best-practices/architectural-patterns.md +75 -53
- package/dist/corpus/wiki/best-practices/code-style-standards/index.md +1 -1
- package/dist/corpus/wiki/best-practices/code-style-standards/tooling.md +6 -2
- package/dist/corpus/wiki/best-practices/contribution-workflow.md +60 -34
- package/dist/corpus/wiki/best-practices/data-modeling.md +1 -1
- package/dist/corpus/wiki/best-practices/deployment-strategies.md +1 -1
- package/dist/corpus/wiki/best-practices/security-guidelines.md +2 -2
- package/dist/corpus/wiki/extensions/atlas/index.md +8 -3
- package/dist/corpus/wiki/extensions/components/api-reference.md +5 -3
- package/dist/corpus/wiki/extensions/components/authentication/api.md +60 -30
- package/dist/corpus/wiki/extensions/components/authentication/errors.md +5 -2
- package/dist/corpus/wiki/extensions/components/authentication/index.md +2 -2
- package/dist/corpus/wiki/extensions/components/authentication/usage.md +4 -1
- package/dist/corpus/wiki/extensions/components/authorization/api.md +127 -49
- package/dist/corpus/wiki/extensions/components/authorization/getting-started.md +18 -7
- package/dist/corpus/wiki/extensions/components/authorization/usage.md +5 -2
- package/dist/corpus/wiki/extensions/components/health-check.md +26 -6
- package/dist/corpus/wiki/extensions/components/index.md +14 -12
- package/dist/corpus/wiki/extensions/components/mail/api.md +72 -9
- package/dist/corpus/wiki/extensions/components/mail/errors.md +25 -6
- package/dist/corpus/wiki/extensions/components/mail/index.md +20 -16
- package/dist/corpus/wiki/extensions/components/mail/usage.md +1 -1
- package/dist/corpus/wiki/extensions/components/request-tracker.md +7 -1
- package/dist/corpus/wiki/extensions/components/socket-io/api.md +2 -1
- package/dist/corpus/wiki/extensions/components/static-asset/api.md +288 -98
- package/dist/corpus/wiki/extensions/components/static-asset/errors.md +45 -28
- package/dist/corpus/wiki/extensions/components/static-asset/index.md +11 -6
- package/dist/corpus/wiki/extensions/components/static-asset/usage.md +45 -15
- package/dist/corpus/wiki/extensions/components/template/api-page.md +9 -5
- package/dist/corpus/wiki/extensions/components/template/index.md +13 -4
- package/dist/corpus/wiki/extensions/components/template/setup-page.md +22 -9
- package/dist/corpus/wiki/extensions/components/template/single-page.md +25 -9
- package/dist/corpus/wiki/extensions/helpers/cron/index.md +1 -1
- package/dist/corpus/wiki/extensions/helpers/crypto/index.md +20 -6
- package/dist/corpus/wiki/extensions/helpers/crypto/reference.md +54 -11
- package/dist/corpus/wiki/extensions/helpers/error/index.md +8 -2
- package/dist/corpus/wiki/extensions/helpers/index.md +15 -3
- package/dist/corpus/wiki/extensions/helpers/inversion/index.md +16 -1
- package/dist/corpus/wiki/extensions/helpers/inversion/reference.md +94 -14
- package/dist/corpus/wiki/extensions/helpers/kafka/admin.md +6 -3
- package/dist/corpus/wiki/extensions/helpers/kafka/consumer.md +17 -4
- package/dist/corpus/wiki/extensions/helpers/kafka/examples.md +2 -2
- package/dist/corpus/wiki/extensions/helpers/kafka/index.md +4 -2
- package/dist/corpus/wiki/extensions/helpers/kafka/producer.md +19 -9
- package/dist/corpus/wiki/extensions/helpers/kafka/schema-registry.md +12 -5
- package/dist/corpus/wiki/extensions/helpers/logger/reference.md +1 -1
- package/dist/corpus/wiki/extensions/helpers/network/api.md +102 -1
- package/dist/corpus/wiki/extensions/helpers/queue/index.md +6 -6
- package/dist/corpus/wiki/extensions/helpers/redis/reference.md +8 -7
- package/dist/corpus/wiki/extensions/helpers/socket-io/api.md +1 -1
- package/dist/corpus/wiki/extensions/helpers/socket-io/index.md +2 -1
- package/dist/corpus/wiki/extensions/helpers/storage/api.md +484 -226
- package/dist/corpus/wiki/extensions/helpers/storage/index.md +33 -16
- package/dist/corpus/wiki/extensions/helpers/types/index.md +4 -1
- package/dist/corpus/wiki/extensions/helpers/types/reference.md +155 -3
- package/dist/corpus/wiki/extensions/helpers/uid/index.md +2 -2
- package/dist/corpus/wiki/extensions/helpers/websocket/api.md +16 -3
- package/dist/corpus/wiki/extensions/helpers/websocket/index.md +3 -3
- package/dist/corpus/wiki/extensions/helpers/worker-thread/index.md +9 -1
- package/dist/corpus/wiki/extensions/index.md +14 -12
- package/dist/corpus/wiki/guides/core-concepts/application/bootstrapping.md +21 -8
- package/dist/corpus/wiki/guides/core-concepts/application/build-info.md +147 -0
- package/dist/corpus/wiki/guides/core-concepts/application/index.md +37 -15
- package/dist/corpus/wiki/guides/core-concepts/components-guide.md +1 -0
- package/dist/corpus/wiki/guides/core-concepts/components.md +8 -6
- package/dist/corpus/wiki/guides/core-concepts/dependency-injection.md +1 -1
- package/dist/corpus/wiki/guides/core-concepts/grpc-controllers.md +8 -4
- package/dist/corpus/wiki/guides/core-concepts/persistent/datasources.md +33 -21
- package/dist/corpus/wiki/guides/core-concepts/persistent/index.md +2 -2
- package/dist/corpus/wiki/guides/core-concepts/persistent/models.md +3 -3
- package/dist/corpus/wiki/guides/core-concepts/persistent/postgres-drivers.md +13 -7
- package/dist/corpus/wiki/guides/core-concepts/persistent/search-meilisearch.md +4 -2
- package/dist/corpus/wiki/guides/core-concepts/persistent/search-typesense.md +21 -8
- package/dist/corpus/wiki/guides/core-concepts/persistent/transactions.md +5 -5
- package/dist/corpus/wiki/guides/core-concepts/rest-controllers.md +2 -1
- package/dist/corpus/wiki/guides/core-concepts/services.md +3 -1
- package/dist/corpus/wiki/guides/get-started/philosophy.md +4 -2
- package/dist/corpus/wiki/guides/migrations/unified-connectors-migration.md +1 -1
- package/dist/corpus/wiki/guides/reference/glossary.md +3 -3
- package/dist/corpus/wiki/guides/tutorials/building-a-crud-api.md +16 -8
- package/dist/corpus/wiki/guides/tutorials/complete-installation.md +33 -22
- package/dist/corpus/wiki/guides/tutorials/ecommerce-api.md +53 -42
- package/dist/corpus/wiki/guides/tutorials/realtime-chat.md +60 -49
- package/dist/corpus/wiki/guides/tutorials/testing.md +18 -8
- package/dist/corpus/wiki/references/base/application.md +129 -45
- package/dist/corpus/wiki/references/base/bootstrapping.md +127 -27
- package/dist/corpus/wiki/references/base/components.md +61 -26
- package/dist/corpus/wiki/references/base/connectors.md +44 -25
- package/dist/corpus/wiki/references/base/controllers.md +20 -9
- package/dist/corpus/wiki/references/base/datasources-reference.md +139 -61
- package/dist/corpus/wiki/references/base/datasources.md +13 -7
- package/dist/corpus/wiki/references/base/dependency-injection.md +34 -14
- package/dist/corpus/wiki/references/base/filter-system/default-filter.md +9 -6
- package/dist/corpus/wiki/references/base/filter-system/fields-order-pagination.md +36 -6
- package/dist/corpus/wiki/references/base/filter-system/index.md +2 -2
- package/dist/corpus/wiki/references/base/filter-system/logical-operators.md +1 -1
- package/dist/corpus/wiki/references/base/filter-system/pattern-matching.md +8 -5
- package/dist/corpus/wiki/references/base/filter-system/quick-reference.md +15 -3
- package/dist/corpus/wiki/references/base/filter-system/tips.md +13 -4
- package/dist/corpus/wiki/references/base/filter-system/use-cases.md +8 -3
- package/dist/corpus/wiki/references/base/grpc-controllers.md +20 -4
- package/dist/corpus/wiki/references/base/index.md +37 -18
- package/dist/corpus/wiki/references/base/middlewares.md +108 -42
- package/dist/corpus/wiki/references/base/models-reference.md +17 -7
- package/dist/corpus/wiki/references/base/models.md +1 -1
- package/dist/corpus/wiki/references/base/providers.md +29 -10
- package/dist/corpus/wiki/references/base/repositories/advanced.md +42 -20
- package/dist/corpus/wiki/references/base/repositories/index.md +10 -5
- package/dist/corpus/wiki/references/base/repositories/mixins.md +5 -5
- package/dist/corpus/wiki/references/base/repositories/soft-deletable.md +22 -12
- package/dist/corpus/wiki/references/base/secrets.md +9 -6
- package/dist/corpus/wiki/references/base/services.md +46 -14
- package/dist/corpus/wiki/references/configuration/environment-variables.md +52 -9
- package/dist/corpus/wiki/references/configuration/index.md +13 -6
- package/dist/corpus/wiki/references/quick-reference.md +79 -33
- package/dist/corpus/wiki/references/utilities/module.md +19 -3
- package/dist/corpus/wiki/references/utilities/parse.md +2 -0
- package/dist/corpus/wiki/references/utilities/schema.md +4 -2
- package/package.json +2 -2
|
@@ -209,6 +209,6 @@ All changes are additive enhancements. Existing filter queries work unchanged.
|
|
|
209
209
|
## Documentation
|
|
210
210
|
|
|
211
211
|
Full documentation with examples available at:
|
|
212
|
-
- [Repositories - JSON Path Filtering](/references/base/
|
|
213
|
-
- [Repositories - Array Column Operators](/references/base/
|
|
214
|
-
- [Repositories - Query Operators](/references/base/
|
|
212
|
+
- [Repositories - JSON Path Filtering](/references/base/filter-system/json-filtering)
|
|
213
|
+
- [Repositories - Array Column Operators](/references/base/filter-system/array-operators)
|
|
214
|
+
- [Repositories - Query Operators](/references/base/filter-system/quick-reference)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: An Application Lists None Of Its Own Artifacts
|
|
3
|
+
description: "configs.discoverArtifacts: true registers every decorated class in the import graph, so a stereotype is the whole declaration. when still keeps several applications in one process apart."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## An application lists none of its own artifacts
|
|
9
|
+
|
|
10
|
+
<Badge type="tip" text="Feature" />
|
|
11
|
+
|
|
12
|
+
**In one line.** Set `discoverArtifacts: true`, import the generated file for its side effects, and
|
|
13
|
+
every decorated class registers itself.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// index.ts - side effects only. No list, no export to name.
|
|
17
|
+
import './generated/artifacts';
|
|
18
|
+
|
|
19
|
+
// application.ts
|
|
20
|
+
export const beConfigs: IApplicationConfigs = {
|
|
21
|
+
path: { base: '/', isStrict: true },
|
|
22
|
+
discoverArtifacts: true, // no list of services, repositories, components or configurations
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`@service()` on the class is the whole declaration. Before, the decorator said what a class is and
|
|
27
|
+
`configs.artifacts` separately said the class exists - two statements for one fact, and the second
|
|
28
|
+
was hand-maintained.
|
|
29
|
+
|
|
30
|
+
## Why discovery happens at import, not at boot
|
|
31
|
+
|
|
32
|
+
A class exists at run time only because something imported it. That rules out the boot-time
|
|
33
|
+
filesystem scan other frameworks use: a compiled single-file binary has no source tree to walk, and
|
|
34
|
+
IGNIS removed that machinery on purpose.
|
|
35
|
+
|
|
36
|
+
So `injectable` - the one function `@service`, `@repository`, `@controller`, `@datasource`,
|
|
37
|
+
`@component`, `@configuration` and `@model` all call - records the class as it decorates it. The
|
|
38
|
+
generated file already imports every one of those classes, so importing it is what makes them exist.
|
|
39
|
+
|
|
40
|
+
`@model` is recorded and then skipped: a model has a binding namespace but no index field, because
|
|
41
|
+
its repository registers it.
|
|
42
|
+
|
|
43
|
+
## Discovery is off until you ask, and that is deliberate
|
|
44
|
+
|
|
45
|
+
| `discoverArtifacts` | `artifacts` | What registers |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| unset | unset | nothing - unchanged |
|
|
48
|
+
| unset | set | exactly what it lists - unchanged |
|
|
49
|
+
| `true` | unset | every decorated class |
|
|
50
|
+
| `true` | set | the union, de-duplicated |
|
|
51
|
+
|
|
52
|
+
**An absent `artifacts` still registers nothing.** Reading it as "register everything" was the first
|
|
53
|
+
shape of this change, and it is a trap: a consumer whose `createAppConfig` omits the key on purpose
|
|
54
|
+
would have booted with every decorated class in the binary - no compile error, no warning, just ten
|
|
55
|
+
times the bindings. One of its applications does exactly that, deliberately, to register nothing.
|
|
56
|
+
|
|
57
|
+
The two compose because a library exports an index and an application still wants to discover its own
|
|
58
|
+
classes beside it. A class named in both registers once.
|
|
59
|
+
|
|
60
|
+
## Several applications in one process
|
|
61
|
+
|
|
62
|
+
The discovered list is process-wide, so without a filter every application registers everything.
|
|
63
|
+
`when` already carries this, and already receives the application:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
@controller({ when: ({ application }) => application instanceof SearchApplication })
|
|
67
|
+
class OrderController {}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**A class with no `when` registers in every application of the process.** That is the cost, and it is
|
|
71
|
+
why a worker that must not mount controllers keeps using `when`.
|
|
72
|
+
|
|
73
|
+
## Framework components stay explicit
|
|
74
|
+
|
|
75
|
+
No class inside IGNIS carries a stereotype, so discovery never turns on a framework component behind
|
|
76
|
+
your back. Choosing which features an application runs is a real decision, and it stays one:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
preConfigure() {
|
|
80
|
+
this.component(HealthCheckComponent);
|
|
81
|
+
this.component(StaticAssetComponent);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Measured on the reference application
|
|
86
|
+
|
|
87
|
+
`examples/vert` lists none of its own. A probe booting it finds 2 services, 10 repositories, 3
|
|
88
|
+
controllers, 1 datasource and 1 component - the same 17 classes its generated index listed, class for
|
|
89
|
+
class.
|
|
90
|
+
|
|
91
|
+
## Who is affected
|
|
92
|
+
|
|
93
|
+
**You set `configs.artifacts`.** Nothing. Same behaviour, same classes, same order.
|
|
94
|
+
|
|
95
|
+
**You want to stop listing.** Replace `import { GeneratedArtifacts } from './generated/artifacts'`
|
|
96
|
+
with `import './generated/artifacts'`, set `discoverArtifacts: true`, delete the `artifacts` entry,
|
|
97
|
+
and move any framework component into `preConfigure()`.
|
|
98
|
+
|
|
99
|
+
**You run several applications in one process.** Read the `when` section before setting the flag.
|
|
100
|
+
|
|
101
|
+
**Files:**
|
|
102
|
+
|
|
103
|
+
- [`packages/kernel/src/base/metadata/injectable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/kernel/src/base/metadata/injectable.ts) - records the class it decorates
|
|
104
|
+
- [`packages/kernel/src/base/applications/artifact-index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/kernel/src/base/applications/artifact-index.ts) - `buildDiscoveredIndex`
|
|
105
|
+
- [`packages/kernel/src/base/applications/rest.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/kernel/src/base/applications/rest.ts) - `registerDiscoveredArtifacts`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Asset Options Take A Resolver Anywhere They Took A Value
|
|
3
|
+
description: "controller.bucket, metaLink.model and metaLink.repository accept a value or a function returning one; TResolveObjectName is renamed TObjectNameResolver, following the verb-last naming the repository already uses."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## Asset options take a resolver anywhere they took a value
|
|
9
|
+
|
|
10
|
+
<Badge type="danger" text="Breaking" />
|
|
11
|
+
|
|
12
|
+
**In one line.** One type is renamed, and three options widen to `TValueOrAsyncResolver`.
|
|
13
|
+
|
|
14
|
+
| Before | After |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `TResolveObjectName` | `TObjectNameResolver` |
|
|
17
|
+
| `controller.bucket?: string \| (() => string)` | `controller.bucket?: TValueOrAsyncResolver<string>` |
|
|
18
|
+
| `metaLink.model: typeof BaseRelationalEntity<Schema>` | `metaLink.model: TValueOrAsyncResolver<...>` |
|
|
19
|
+
| `metaLink.repository: DefaultCRUDRepository<Schema>` | `metaLink.repository: TValueOrAsyncResolver<...>` |
|
|
20
|
+
|
|
21
|
+
The rename is the breaking half. `TResolveObjectName` read verb-first, which named an action; every
|
|
22
|
+
other type in the repository names a thing. The shape is untouched, so the fix is the new name.
|
|
23
|
+
|
|
24
|
+
The route table also gained a name of its own, `TStaticAssetRoutes`, lifted out of the inline
|
|
25
|
+
`controller.routes` shape. Nothing about it changed.
|
|
26
|
+
|
|
27
|
+
The other breaking change landing today touches the same component: the download route drops its
|
|
28
|
+
plural, `/downloads/{objectName}` becomes
|
|
29
|
+
[`/download/{objectName}`](./2026-09-14-download-route-singular).
|
|
30
|
+
|
|
31
|
+
## Why a resolver, and what it buys
|
|
32
|
+
|
|
33
|
+
A resolver is read when the value is used, not when the options object is built. That is what lets a
|
|
34
|
+
repository be named before the container has bound it, and what lets a bucket come from an
|
|
35
|
+
environment variable read per request rather than at boot:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
controller: { name: 'AssetController', basePath: '/assets', bucket: () => process.env.APP_ENV_BUCKET },
|
|
39
|
+
metaLink: {
|
|
40
|
+
model: MetaLinkModel,
|
|
41
|
+
repository: () => application.get<MetaLinkRepository>({ key: 'repositories.MetaLinkRepository' }),
|
|
42
|
+
},
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A plain value still works everywhere. A class is treated as a value, never as a factory, so
|
|
46
|
+
`model: MetaLinkModel` is never called.
|
|
47
|
+
|
|
48
|
+
## Who is affected
|
|
49
|
+
|
|
50
|
+
**You name `TResolveObjectName`.** Rename it. The compiler finds every site.
|
|
51
|
+
|
|
52
|
+
**You pass a plain bucket, model or repository.** Nothing. A value is still a value.
|
|
53
|
+
|
|
54
|
+
**You wrote `bucket: () => env`.** Nothing. That form already worked and is now the documented shape.
|
|
55
|
+
|
|
56
|
+
**Files:**
|
|
57
|
+
|
|
58
|
+
- [`packages/core-server/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/types.ts) - `TObjectNameResolver`, `TStaticAssetRoutes`, `TMetaLinkConfig`
|
|
59
|
+
- [`packages/core-server/src/components/static-asset/controller/factory.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/controller/factory.ts) - `resolveBucket`
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A Component Reads Its Options In binding(), Without Overriding configure()
|
|
3
|
+
description: BaseComponent stores what application.component(Ctor, { options }) handed it on this.configuredOptions before binding() runs. StaticAssetComponent takes its options that way.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## A component reads its options in `binding()`
|
|
9
|
+
|
|
10
|
+
<Badge type="tip" text="Enhancement" />
|
|
11
|
+
|
|
12
|
+
**In one line.** `BaseComponent` keeps the options it was configured with on
|
|
13
|
+
`this.configuredOptions`, set before `binding()` runs.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
class MailComponent extends BaseComponent<IMailOptions> {
|
|
17
|
+
override binding() {
|
|
18
|
+
const { provider } = this.configuredOptions ?? {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
application.component(MailComponent, { options: { provider: 'amazon-ses' } });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Until now the options reached `configure()` and stopped there: `binding()` had no way to read them,
|
|
26
|
+
so every component that wanted them overrode `configure()` to stash a copy. Each one invented its
|
|
27
|
+
own field name.
|
|
28
|
+
|
|
29
|
+
## `StaticAssetComponent` takes its options directly
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
application.component(StaticAssetComponent, {
|
|
33
|
+
options: {
|
|
34
|
+
assets: {
|
|
35
|
+
controller: { name: 'AssetController', basePath: '/assets' },
|
|
36
|
+
storage: StaticAssetStorageTypes.BUN_S3,
|
|
37
|
+
helper: storageHelper,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS` still works, and is still the right
|
|
44
|
+
answer in one case: a component registered through `configs.artifacts` gets no options, because an
|
|
45
|
+
artifact index carries classes and nothing else. Direct options win when both are present.
|
|
46
|
+
|
|
47
|
+
## Who is affected
|
|
48
|
+
|
|
49
|
+
**You bind `STATIC_ASSET_COMPONENT_OPTIONS`.** Nothing. The key is read whenever no options are
|
|
50
|
+
passed.
|
|
51
|
+
|
|
52
|
+
**You register `StaticAssetComponent` through `configs.artifacts`.** Nothing, and the key stays the
|
|
53
|
+
way to configure it - through a `@provide` method on a component of your own, for example.
|
|
54
|
+
|
|
55
|
+
**You write your own component.** Drop the `configure()` override that existed only to keep a copy
|
|
56
|
+
of the options.
|
|
57
|
+
|
|
58
|
+
**Files:**
|
|
59
|
+
|
|
60
|
+
- [`packages/kernel/src/base/components/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/kernel/src/base/components/base.ts) - `BaseComponent.configuredOptions`
|
|
61
|
+
- [`packages/core-server/src/components/static-asset/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/component.ts) - `StaticAssetComponent.binding`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The Download Route Drops Its Plural
|
|
3
|
+
description: GET /downloads/{objectName} becomes GET /download/{objectName}. A verb segment names one action, not a collection, and this reverses one row of the 2026-09-08 rename.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## The download route drops its plural
|
|
9
|
+
|
|
10
|
+
<Badge type="danger" text="Breaking" />
|
|
11
|
+
|
|
12
|
+
**In one line.** The static-asset download route is now singular.
|
|
13
|
+
|
|
14
|
+
| Before | After |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `GET {base}/downloads/{objectName}` | `GET {base}/download/{objectName}` |
|
|
17
|
+
|
|
18
|
+
Nothing else moves. `{base}` is `/buckets/{bucketName}` unless `controller.bucket` is configured.
|
|
19
|
+
|
|
20
|
+
## Why the plural was wrong
|
|
21
|
+
|
|
22
|
+
`/buckets` and `/objects` are plural because each names a collection you can list. `/download` names
|
|
23
|
+
neither - it is the action you take on one object, and the object is already in the next segment.
|
|
24
|
+
The [2026-09-08 rename](./2026-09-08-storage-hardening-and-bun-s3-only) argued the segment named "a
|
|
25
|
+
set of representations". It does not. There is one representation, and one verb.
|
|
26
|
+
|
|
27
|
+
`POST {base}/objects` for upload is unaffected, and correct for the same reason: the collection is
|
|
28
|
+
the resource, and `POST` is already the verb.
|
|
29
|
+
|
|
30
|
+
The action still sits **before** the key, and that part is forced rather than chosen. With nested
|
|
31
|
+
object keys enabled, a suffix route never matches - Hono serves `/objects/photos/a.png/download`
|
|
32
|
+
through the catch-all with the key `photos/a.png/download`.
|
|
33
|
+
|
|
34
|
+
## Who is affected
|
|
35
|
+
|
|
36
|
+
**You never called the download route.** Nothing.
|
|
37
|
+
|
|
38
|
+
**You are on `0.2.0-31` and call `/downloads/{objectName}`.** Drop the `s`. A client on the old path
|
|
39
|
+
gets a `404` at runtime; nothing fails at compile time.
|
|
40
|
+
|
|
41
|
+
**You pin the path yourself.** An override wins over both spellings:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
application.component(StaticAssetComponent, {
|
|
45
|
+
options: {
|
|
46
|
+
controller: {
|
|
47
|
+
routes: { downloadObjectByName: { path: '/download/{objectName}{.+}' } },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The route key `downloadObjectByName` and the handler name are unchanged - only the URL segment moved.
|
|
54
|
+
|
|
55
|
+
**Files:**
|
|
56
|
+
|
|
57
|
+
- [`packages/core-server/src/components/static-asset/controller/base.definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/controller/base.definition.ts) - `DOWNLOAD_OBJECT_BY_NAME`
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A Dependency Reached Through An Import Cycle Can Still Name Its Class
|
|
3
|
+
description: "@inject({ target }) takes a function returning the class. The function runs when the container resolves, so a class a barrel makes unreachable at module load is still nameable."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## A dependency reached through an import cycle can still name its class
|
|
9
|
+
|
|
10
|
+
<Badge type="tip" text="Feature" />
|
|
11
|
+
|
|
12
|
+
**In one line.** `@inject({ target })` accepts a function that returns the class, and the function
|
|
13
|
+
runs at resolve time.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
@inject({ target: () => TaxOnboardingService })
|
|
17
|
+
private readonly taxOnboardingService: TaxOnboardingService
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`@inject({ target: TaxOnboardingService })` is unchanged and stays the default. Reach for the
|
|
21
|
+
function form only where a barrel makes the class unreachable at module load.
|
|
22
|
+
|
|
23
|
+
## The failure it answers
|
|
24
|
+
|
|
25
|
+
A barrel re-exports two modules and one imports the other back. The module the barrel evaluates
|
|
26
|
+
first reads a class that is still in its temporal dead zone:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
ReferenceError: Cannot access 'TaxOnboardingService' before initialization
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That error comes from the module system, before `inject` is ever called, so the framework cannot
|
|
33
|
+
name the decorator, the class, or the parameter. The only fix available today is to abandon the
|
|
34
|
+
barrel and deep-import - which works only when both files are yours.
|
|
35
|
+
|
|
36
|
+
## The annotation has to move too
|
|
37
|
+
|
|
38
|
+
The thunk covers the decorator argument. It does not cover the type annotation, because
|
|
39
|
+
`emitDecoratorMetadata` turns that annotation into a second, eager value reference:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
__legacyMetadataTS('design:paramtypes', [typeof NoteService === 'undefined' ? Object : NoteService]);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`typeof` on a binding in the temporal dead zone throws too, so TypeScript's own guard does not help.
|
|
46
|
+
Import the class twice - once as a value for the thunk, once as a type for the annotation:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { NoteService } from './index';
|
|
50
|
+
import type { NoteService as TNoteService } from './index';
|
|
51
|
+
|
|
52
|
+
constructor(@inject({ target: () => NoteService }) readonly noteService: TNoteService) {}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Worth knowing even if you never use the thunk: **`@inject({ key })` dies on the same cycle**, for the
|
|
56
|
+
same reason. The plural spelling of the problem is `emitDecoratorMetadata`, not the class form.
|
|
57
|
+
|
|
58
|
+
## There is no async form, on purpose
|
|
59
|
+
|
|
60
|
+
`Container.instantiate` is synchronous and ends in `new cls(...args)`. An async resolver would make
|
|
61
|
+
it async, then `get()` async, then every `@inject` site async. The target is only read to look up a
|
|
62
|
+
binding key recorded on the class, so there is nothing to await.
|
|
63
|
+
|
|
64
|
+
## The resolver vocabulary moved down a layer
|
|
65
|
+
|
|
66
|
+
`TResolver`, `TAsyncResolver`, `TValueOrResolver`, `TValueOrAsyncResolver`, `resolveValue` and
|
|
67
|
+
`resolveValueAsync` now live in `@venizia/ignis-inversion`, beside the container that branches on
|
|
68
|
+
them. `@venizia/ignis-helpers/common` re-exports all six, so every existing import keeps working and
|
|
69
|
+
there is one declaration instead of two.
|
|
70
|
+
|
|
71
|
+
## Who is affected
|
|
72
|
+
|
|
73
|
+
**You use `@inject({ key })` or `@inject({ target: Class })`.** Nothing. Both are unchanged.
|
|
74
|
+
|
|
75
|
+
**You hit a circular-import crash and worked around it by deep-importing.** The thunk plus a
|
|
76
|
+
type-only annotation replaces the workaround.
|
|
77
|
+
|
|
78
|
+
**You import a resolver type from `@venizia/ignis-helpers/common`.** Nothing. The names and shapes
|
|
79
|
+
are identical.
|
|
80
|
+
|
|
81
|
+
**Files:**
|
|
82
|
+
|
|
83
|
+
- [`packages/inversion/src/common/utilities.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/common/utilities.ts) - `resolveValue`, `resolveValueAsync`, `resolveInjectTarget`
|
|
84
|
+
- [`packages/inversion/src/modules/container/container.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/container/container.ts) - `Container.resolveBindingKey`
|
|
85
|
+
- [`packages/inversion/src/modules/metadata/injectors.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/metadata/injectors.ts) - `TInjectOptions`
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The MetaLink Table Can Live In Your Own Postgres Schema
|
|
3
|
+
description: TMetaLinkConfig pinned the MetaLink type to the exact table IGNIS ships, so a column-identical table under another Postgres schema was refused at compile time. The constraint is now the row, not the table.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## The MetaLink table can live in your own Postgres schema
|
|
9
|
+
|
|
10
|
+
<Badge type="tip" text="Fix" />
|
|
11
|
+
|
|
12
|
+
**In one line.** `TStaticAssetsComponentOptions` takes the MetaLink table as a type argument, and the
|
|
13
|
+
constraint is now the row rather than the exact table IGNIS ships.
|
|
14
|
+
|
|
15
|
+
A column-for-column identical table under a Postgres schema of your own used to be refused:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
error TS2322: Types of property 'schema' are incompatible.
|
|
19
|
+
Type '"commerce"' is not assignable to type 'undefined'.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Nothing was wrong at runtime. The repository carries its own schema, so the row landed in
|
|
23
|
+
`commerce.MetaLink` exactly as intended. Only the type layer said no, and an application had no way
|
|
24
|
+
around it - the option type never took a type argument, so declaring one on `TMetaLinkConfig`
|
|
25
|
+
changed nothing.
|
|
26
|
+
|
|
27
|
+
## Pass your table as the type argument
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const commerceSchema = pgSchema('commerce');
|
|
31
|
+
|
|
32
|
+
@model({ type: 'entity' })
|
|
33
|
+
export class MetaLinkModel extends BaseRelationalEntity<typeof MetaLinkModel.schema> {
|
|
34
|
+
static override schema = commerceSchema.table('MetaLink', { /* the MetaLink columns */ });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.bind<TStaticAssetsComponentOptions<typeof MetaLinkModel.schema>>({
|
|
38
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
39
|
+
}).toValue({ /* ... */ });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The new constraint is `TMetaLinkCompatibleSchema`: any table whose row carries the MetaLink fields.
|
|
43
|
+
The table is free to sit in another Postgres schema, or to carry another name.
|
|
44
|
+
|
|
45
|
+
**A table missing one of those fields is still refused.** That is the part worth keeping - the
|
|
46
|
+
workaround people reach for, `as unknown as TMetaLinkConfig['model']`, silences the missing-column
|
|
47
|
+
check along with the schema complaint.
|
|
48
|
+
|
|
49
|
+
## `createMetaLink` returns a row, and now says so
|
|
50
|
+
|
|
51
|
+
Its return type read `{ count: number; data: Schema }`, where `Schema` is the TABLE type. The
|
|
52
|
+
component reads a row out of it, and `repository.create()` returns a row, so the annotation named the
|
|
53
|
+
wrong thing. It is `{ count: number; data: TTableObject<Schema> }`.
|
|
54
|
+
|
|
55
|
+
If you wrote `createMetaLink` and fought the type, the fight is over. Nothing about the value changed.
|
|
56
|
+
|
|
57
|
+
## The class is generic too, not only the options
|
|
58
|
+
|
|
59
|
+
`component(Ctor, { options })` infers the options from the CLASS, so a class pinned to the shipped
|
|
60
|
+
table can only ever carry options for that table. Making the option types generic fixed the
|
|
61
|
+
`bind<>()` path and left the `component()` path failing:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
error TS2345: Argument of type 'typeof StaticAssetComponent' is not assignable to parameter of type
|
|
65
|
+
'TClass<BaseComponent<{ asset: { controller: ... }}>>'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`StaticAssetComponent<Schema>` carries the table now, so both paths take a namespaced table:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
application.component(StaticAssetComponent, { options: { assets: { /* ... */ } } });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Inside the controller factory the table type is erased on purpose: every read and write there is on
|
|
75
|
+
MetaLink COLUMNS, and threading a table type through 700 lines of handler buys nothing the public
|
|
76
|
+
option types do not already check.
|
|
77
|
+
|
|
78
|
+
## Who is affected
|
|
79
|
+
|
|
80
|
+
**You do not use `useMetaLink`.** Nothing.
|
|
81
|
+
|
|
82
|
+
**You use the MetaLink table IGNIS ships.** Nothing. `TStaticAssetsComponentOptions` with no type
|
|
83
|
+
argument keeps meaning what it meant.
|
|
84
|
+
|
|
85
|
+
**You kept your MetaLink table in another Postgres schema behind a cast.** Drop the cast and pass the
|
|
86
|
+
table as the type argument. The missing-column check comes back with it.
|
|
87
|
+
|
|
88
|
+
**Files:**
|
|
89
|
+
|
|
90
|
+
- [`packages/core-server/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/types.ts) - `TMetaLinkConfig`, `TStaticAssetsComponentOptions`
|
|
91
|
+
- [`packages/core-server/src/components/static-asset/models/base.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/models/base.model.ts) - `TMetaLinkCompatibleSchema`
|
|
92
|
+
- [`examples/vert/src/components/platform.component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/examples/vert/src/components/platform.component.ts) - a working `useMetaLink: true` example
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The Stats Key Header Shows Up In The API Reference
|
|
3
|
+
description: GET /health/stats now declares x-health-key in its OpenAPI definition, so the API reference renders an input for it instead of answering 404 with no way to send the key.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## The stats key header shows up in the API reference
|
|
9
|
+
|
|
10
|
+
<Badge type="tip" text="Enhancement" />
|
|
11
|
+
|
|
12
|
+
**In one line.** `GET /health/stats` declares `x-health-key` as a header parameter, so the API
|
|
13
|
+
reference renders a box for it.
|
|
14
|
+
|
|
15
|
+
Before this, a host that set `stats.secretKey` locked the route behind a header the reference never
|
|
16
|
+
offered. Opening `/health/stats` from the explorer returned `404` with no way to send the key, and
|
|
17
|
+
the `404` is deliberate - an unauthorised caller must not learn the route exists - so the reference
|
|
18
|
+
gave no hint about what was missing.
|
|
19
|
+
|
|
20
|
+
## The header stays optional
|
|
21
|
+
|
|
22
|
+
A host with no `stats.secretKey` runs an open stats route and needs no key. Marking the parameter
|
|
23
|
+
required would make the reference lie to that host, so it is declared optional and the description
|
|
24
|
+
carries the condition:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
x-health-key string, optional
|
|
28
|
+
The stats key. A host that configures `stats.secretKey` answers 404 without it.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Who is affected
|
|
32
|
+
|
|
33
|
+
**You set `stats.secretKey`.** The reference now has a box for the key. Nothing changes on the wire.
|
|
34
|
+
|
|
35
|
+
**You run an open stats route.** One optional parameter appears in the reference. Requests are
|
|
36
|
+
unaffected - the value is never read when no key is configured.
|
|
37
|
+
|
|
38
|
+
**You call the route from code.** Nothing changes. The header name, the gate and the `404` are the
|
|
39
|
+
same.
|
|
40
|
+
|
|
41
|
+
**Files:**
|
|
42
|
+
|
|
43
|
+
- [`packages/core-server/src/components/health-check/controller.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/health-check/controller.ts) - `RouteConfigs.STATS`
|
|
44
|
+
- [`packages/core-server/src/components/health-check/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/health-check/common/constants.ts) - `HealthCheckHeaders.SECRET_KEY`
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The Container Can Name A Binding Nobody Resolves
|
|
3
|
+
description: doVerify catches a dependency injected but never registered. The reverse - a class registered that nothing injects - was silent. The container now counts what it hands out.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changelog - 2026-09-14
|
|
7
|
+
|
|
8
|
+
## The container can name a binding nobody resolves
|
|
9
|
+
|
|
10
|
+
<Badge type="tip" text="Feature" />
|
|
11
|
+
|
|
12
|
+
**In one line.** The container counts every key it hands out, so an application can ask which
|
|
13
|
+
registrations nothing ever read.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
await application.initialize();
|
|
17
|
+
application.startResolutionCounting(); // counting is off until you ask
|
|
18
|
+
|
|
19
|
+
// ... run the traffic that exercises the application ...
|
|
20
|
+
|
|
21
|
+
application.getUnresolvedBindings(); // ['repositories.OrphanRepository', ...]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## The asymmetry it closes
|
|
25
|
+
|
|
26
|
+
| The mistake | Before |
|
|
27
|
+
|---|---|
|
|
28
|
+
| A dependency is injected but never registered | boot dies, naming the class - `bootChecks.binding.doVerify` |
|
|
29
|
+
| A class is registered but nothing ever injects it | nothing says anything, ever |
|
|
30
|
+
|
|
31
|
+
So a hand-written artifact list can only grow. Someone adds a repository, a later change removes the
|
|
32
|
+
last site that injects it, and the registration line stays. Measured in one downstream service: 59
|
|
33
|
+
repositories declared by hand, 47 actually injected.
|
|
34
|
+
|
|
35
|
+
A static scan cannot answer this, and that is not a tooling gap. The same service resolves five
|
|
36
|
+
repositories by building a binding key at run time, which no grep sees - a static verdict would
|
|
37
|
+
condemn five live registrations.
|
|
38
|
+
|
|
39
|
+
## Four members, and counting is off until you ask
|
|
40
|
+
|
|
41
|
+
| Member | Answers |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `getResolutionCounts()` | a copy of key to read count; a key bound and never read is absent |
|
|
44
|
+
| `startResolutionCounting()` | clears the counts and starts counting - OFF by default |
|
|
45
|
+
| `stopResolutionCounting()` | stops counting, keeps what was counted |
|
|
46
|
+
| `getUnresolvedBindings({ tags })` | bound keys with no count, defaulting to `services` and `repositories` |
|
|
47
|
+
|
|
48
|
+
**Counting is off by default, and that is the point.** The counter sits on `BaseContainer.get`, the
|
|
49
|
+
one method every injection passes through. Measured over 2 million resolutions: **89 ns with counting
|
|
50
|
+
off, 96 ns with it on.** An application that never reads the report pays nothing.
|
|
51
|
+
|
|
52
|
+
**Start after `initialize()`.** `doVerify: true` reads every service and repository at boot, so
|
|
53
|
+
counting across boot reports empty by construction. Starting is also the reset. Reading the report
|
|
54
|
+
without having started **throws**, naming the method to call - a list that names every binding is a
|
|
55
|
+
wrong answer, not an empty one.
|
|
56
|
+
|
|
57
|
+
## A count of zero is evidence, not a verdict
|
|
58
|
+
|
|
59
|
+
An unread binding may be dead code. It may equally be an allow-list entry, or a technical floor only
|
|
60
|
+
one deployment exercises. The framework cannot tell those apart, so it reports and stops there - no
|
|
61
|
+
warning, no boot gate, no deletion. Read the list yourself.
|
|
62
|
+
|
|
63
|
+
## Who is affected
|
|
64
|
+
|
|
65
|
+
**Nobody, unless you ask.** No new boot step, no new option, no behaviour change, and no cost until
|
|
66
|
+
`startResolutionCounting()` runs.
|
|
67
|
+
|
|
68
|
+
**Files:**
|
|
69
|
+
|
|
70
|
+
- [`packages/inversion/src/modules/container/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/container/base.ts) - `startResolutionCounting`, `getResolutionCounts`
|
|
71
|
+
- [`packages/kernel/src/base/applications/rest.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/kernel/src/base/applications/rest.ts) - `RestApplication.getUnresolvedBindings`
|
|
@@ -17,6 +17,14 @@ This section tracks the history of significant changes, refactors, and updates t
|
|
|
17
17
|
|
|
18
18
|
| Date | Title | Type |
|
|
19
19
|
|------|-------|------|
|
|
20
|
+
| 2026-09-14 | [An Application Declares No Artifacts](./2026-09-14-an-application-declares-no-artifacts) | Feature |
|
|
21
|
+
| 2026-09-14 | [The Container Can Name A Binding Nobody Resolves](./2026-09-14-unresolved-binding-report) | Feature |
|
|
22
|
+
| 2026-09-14 | [Asset Options Take A Resolver Anywhere They Took A Value](./2026-09-14-asset-options-take-resolvers) | Breaking |
|
|
23
|
+
| 2026-09-14 | [A Component Reads Its Options In binding()](./2026-09-14-component-options-reach-binding) | Enhancement |
|
|
24
|
+
| 2026-09-14 | [A Dependency Reached Through An Import Cycle Can Still Name Its Class](./2026-09-14-inject-target-through-an-import-cycle) | Feature |
|
|
25
|
+
| 2026-09-14 | [The MetaLink Table Can Live In Your Own Postgres Schema](./2026-09-14-metalink-table-is-yours) | Fix |
|
|
26
|
+
| 2026-09-14 | [The Stats Key Header Shows Up In The API Reference](./2026-09-14-stats-key-header-in-api-reference) | Enhancement |
|
|
27
|
+
| 2026-09-14 | [The Download Route Drops Its Plural](./2026-09-14-download-route-singular) | Breaking |
|
|
20
28
|
| 2026-09-12 | [An Open Stats Route Now Says So At Boot](./2026-09-12-unguarded-stats-warning) | Enhancement |
|
|
21
29
|
| 2026-09-12 | [Empty Env Values Are Allowed By Default](./2026-09-12-empty-envs-allowed-by-default) | Breaking |
|
|
22
30
|
| 2026-09-12 | [A Blank Env Line No Longer Beats Its Default At Module Load](./2026-09-12-blank-env-at-module-load) | Fix |
|