okengine 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.v1.schema.json +21 -2
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +7 -0
- package/site/content/docs/elements/flow.mdx +20 -12
- package/site/content/docs/elements/gate.mdx +108 -3
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +36 -32
- package/site/content/docs/elements/vault.mdx +26 -0
- package/site/content/docs/get-started/project-structure.mdx +4 -4
- package/site/content/docs/reference/cli.md +1 -1
- package/site/content/docs/reference/client.mdx +75 -17
- package/site/content/docs/reference/configuration.mdx +7 -4
- package/site/content/docs/reference/errors.mdx +24 -17
- package/site/content/docs/reference/fx.mdx +39 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +261 -0
- package/src/auth/api-keys.ts +276 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/config.ts +24 -5
- package/src/auth/index.ts +62 -1
- package/src/auth/plugin.ts +6 -1
- package/src/auth/schema.ts +8 -0
- package/src/auth/sessions.ts +18 -0
- package/src/auth/tables.ts +32 -0
- package/src/auth/tenant-config.ts +74 -0
- package/src/auth/tenant-tables.ts +11 -0
- package/src/auth/tenants.test.ts +63 -0
- package/src/auth/tenants.ts +360 -0
- package/src/cli/attach-host-console.test.ts +37 -0
- package/src/cli/attach-host-console.ts +6 -0
- package/src/cli/build.ts +2 -2
- package/src/client/budget.test.ts +1 -1
- package/src/client/create.ts +75 -5
- package/src/client/index.ts +13 -0
- package/src/client/live.test.ts +422 -0
- package/src/client/live.ts +389 -0
- package/src/client/notes-contract.test.ts +41 -0
- package/src/client/types.ts +85 -6
- package/src/client-react/index.ts +85 -1
- package/src/client-react/use-live.test.ts +129 -0
- package/src/compiler/effects-infer.ts +37 -2
- package/src/compiler/extract.test.ts +143 -11
- package/src/compiler/extract.ts +210 -30
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +2 -3
- package/src/compiler/response.ts +41 -11
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +45 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +113 -58
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +10 -30
- package/src/console/server/index.ts +1 -0
- package/src/console/server/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/state.ts +29 -55
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +43 -1
- package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
- package/src/console/ui-next/dist/index.html +11 -9
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +167 -0
- package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
- package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/journal-postgres.ts +12 -3
- package/src/drivers/pg-rls.ts +26 -2
- package/src/drivers/pg-vault-rls.ts +68 -0
- package/src/drivers/signal-engine.ts +69 -15
- package/src/drivers/signal-live-iter.ts +65 -0
- package/src/drivers/signal-nats.ts +2 -1
- package/src/drivers/signal-postgres.ts +95 -12
- package/src/drivers/signal-redis.ts +2 -1
- package/src/drivers/signal-retention.ts +64 -0
- package/src/drivers/signal-types.ts +35 -5
- package/src/elements/clock/declare.ts +64 -2
- package/src/elements/clock/reconcile.ts +98 -25
- package/src/elements/clock/runtime.ts +13 -2
- package/src/elements/clock.test.ts +28 -0
- package/src/elements/clock.ts +9 -1
- package/src/elements/gate/declare.ts +2 -0
- package/src/elements/gate/permissions.ts +12 -0
- package/src/elements/gate.ts +6 -1
- package/src/elements/signal/declare.ts +44 -10
- package/src/elements/signal/delivery-modes.test.ts +90 -4
- package/src/elements/signal/order-lifecycle.test.ts +20 -4
- package/src/elements/signal/runtime.ts +42 -0
- package/src/elements/signal.test.ts +21 -8
- package/src/elements/signal.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/rls-identity.test.ts +29 -0
- package/src/elements/store/rls-identity.ts +3 -0
- package/src/elements/store/runtime.ts +14 -0
- package/src/elements/store/schema-decl.ts +162 -44
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +54 -32
- package/src/elements/store.ts +5 -0
- package/src/elements/vault/builtin-adapter.ts +15 -2
- package/src/elements/vault/declare.ts +8 -0
- package/src/elements/vault/runtime.ts +7 -0
- package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
- package/src/elements/vault/storage.ts +9 -0
- package/src/elements/vault/test-helpers.ts +2 -1
- package/src/i18n/catalogs/ar.ts +19 -0
- package/src/i18n/catalogs/en.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +196 -57
- package/src/kernel/auth-resolve.ts +13 -5
- package/src/kernel/boot-bind/clock.ts +9 -3
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/clock-durable.ts +8 -0
- package/src/kernel/clock-per-tenant-name.ts +5 -0
- package/src/kernel/clock-reconcile.ts +8 -0
- package/src/kernel/errors-live-resume.ts +15 -0
- package/src/kernel/errors-tenant.ts +29 -0
- package/src/kernel/errors.registry.test.ts +31 -3
- package/src/kernel/errors.ts +43 -3
- package/src/kernel/flow.ts +26 -5
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +203 -0
- package/src/kernel/fx-auth-tenants.test.ts +87 -0
- package/src/kernel/fx-auth-tenants.ts +286 -0
- package/src/kernel/fx-live-stream.ts +149 -0
- package/src/kernel/fx-live.test.ts +157 -0
- package/src/kernel/fx-runtime.ts +15 -0
- package/src/kernel/fx-tenant-store.ts +213 -0
- package/src/kernel/fx.test.ts +108 -1
- package/src/kernel/fx.ts +206 -28
- package/src/kernel/hooks.ts +2 -2
- package/src/kernel/http-resource.ts +9 -18
- package/src/kernel/index.ts +2 -0
- package/src/kernel/journal.ts +12 -0
- package/src/kernel/live-http.test.ts +78 -0
- package/src/kernel/live-http.ts +114 -0
- package/src/kernel/live-resume.test.ts +125 -0
- package/src/kernel/on.ts +51 -0
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +41 -4
- package/src/kernel/resource-mount.test.ts +10 -27
- package/src/kernel/tenant-resolve.test.ts +101 -0
- package/src/kernel/tenant-resolve.ts +124 -0
- package/src/kernel/tenant-roles.test.ts +87 -0
- package/src/kernel/triggers.ts +59 -21
- package/src/manifest/diff.test.ts +11 -2
- package/src/manifest/diff.ts +53 -11
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +0 -1
- package/src/manifest/types.ts +58 -6
- package/src/release/build-lib.ts +14 -1
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +55 -1
- package/src/client/live-gap.test.ts +0 -35
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
- package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ColumnClassification } from "../../manifest/types.ts";
|
|
9
|
+
import { lazyRequire } from "../../kernel/lazy-require.ts";
|
|
9
10
|
import type { ColumnDef, TableHandle } from "./table.ts";
|
|
10
11
|
import { id as idHelper, now as nowHelper } from "./table.ts";
|
|
11
12
|
|
|
@@ -38,8 +39,14 @@ export interface ColumnReference {
|
|
|
38
39
|
* `getSQL` is a structural bridge so drizzle-orm operators (`eq`, `isNull`, …)
|
|
39
40
|
* accept abstract columns in TypeScript. OKE’s SQL compiler never calls it —
|
|
40
41
|
* it reads {@link SchemaColumnDecl.sqlName} / metadata directly.
|
|
42
|
+
*
|
|
43
|
+
* `TData` / `TNotNull` are type-level only (`$infer` is a phantom). They let
|
|
44
|
+
* {@link InferColumnJs} / table `$inferSelect` resolve real row shapes.
|
|
41
45
|
*/
|
|
42
|
-
export interface SchemaColumnDecl
|
|
46
|
+
export interface SchemaColumnDecl<
|
|
47
|
+
TData = unknown,
|
|
48
|
+
TNotNull extends boolean = boolean,
|
|
49
|
+
> extends ColumnDef {
|
|
43
50
|
/** JS object key (e.g. `createdAt`). */
|
|
44
51
|
readonly key: string;
|
|
45
52
|
/** Database column name (e.g. `created_at`). */
|
|
@@ -47,7 +54,12 @@ export interface SchemaColumnDecl extends ColumnDef {
|
|
|
47
54
|
/** Declared SQL type. */
|
|
48
55
|
readonly sqlType: FieldSqlType;
|
|
49
56
|
readonly primaryKey: boolean;
|
|
50
|
-
readonly notNull:
|
|
57
|
+
readonly notNull: TNotNull;
|
|
58
|
+
/**
|
|
59
|
+
* Phantom JS value type — not present at runtime. Distinguishes `string`
|
|
60
|
+
* vs `number` so structural typing does not collapse column data types.
|
|
61
|
+
*/
|
|
62
|
+
readonly $infer?: TData;
|
|
51
63
|
readonly unique: boolean;
|
|
52
64
|
/** Literal `.default(v)` when set. */
|
|
53
65
|
readonly defaultValue?: string | number | boolean | null;
|
|
@@ -93,8 +105,14 @@ export interface SchemaRlsEnableDecl {
|
|
|
93
105
|
readonly kind: "schema-rls";
|
|
94
106
|
}
|
|
95
107
|
|
|
108
|
+
/** Opt out of tenant isolation for a schema table when tenancy is on. */
|
|
109
|
+
export interface SchemaTenantScopedDecl {
|
|
110
|
+
readonly kind: "schema-tenant-scoped";
|
|
111
|
+
readonly tenantScoped: false;
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
/** Third-arg extra for {@link schemaTable}. */
|
|
97
|
-
export type SchemaTableExtra = SchemaPolicyDecl | SchemaRlsEnableDecl;
|
|
115
|
+
export type SchemaTableExtra = SchemaPolicyDecl | SchemaRlsEnableDecl | SchemaTenantScopedDecl;
|
|
98
116
|
|
|
99
117
|
/** Options for {@link schemaPolicy} / Gate helpers. */
|
|
100
118
|
export interface SchemaPolicyOptions {
|
|
@@ -118,35 +136,45 @@ export interface SchemaTableDecl extends TableHandle {
|
|
|
118
136
|
readonly rls?: boolean;
|
|
119
137
|
/** Declared policies (emit + Manifest). */
|
|
120
138
|
readonly policies?: readonly SchemaPolicyDecl[];
|
|
139
|
+
/** When `false`, skip fail-loud tenant-policy requirement. */
|
|
140
|
+
readonly tenantScoped?: boolean;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Fluent field builder before key finalization.
|
|
145
|
+
*
|
|
146
|
+
* `TData` is the JS value type (`string` / `number`). `TNotNull` is `true`
|
|
147
|
+
* after `.notNull()` or `.primaryKey()` — matching runtime finalize.
|
|
148
|
+
*/
|
|
149
|
+
export interface FieldBuilder<TData = unknown, TNotNull extends boolean = false> {
|
|
150
|
+
primaryKey(): FieldBuilder<TData, true>;
|
|
151
|
+
notNull(): FieldBuilder<TData, true>;
|
|
152
|
+
unique(): FieldBuilder<TData, TNotNull>;
|
|
153
|
+
default(value: string | number | boolean | null): FieldBuilder<TData, TNotNull>;
|
|
154
|
+
defaultFn(fn: () => unknown): FieldBuilder<TData, TNotNull>;
|
|
155
|
+
pii(): FieldBuilder<TData, TNotNull>;
|
|
156
|
+
sensitive(): FieldBuilder<TData, TNotNull>;
|
|
157
|
+
retain(duration: string): FieldBuilder<TData, TNotNull>;
|
|
133
158
|
/** Override snake_case SQL name. */
|
|
134
|
-
as(sqlName: string): FieldBuilder
|
|
159
|
+
as(sqlName: string): FieldBuilder<TData, TNotNull>;
|
|
135
160
|
/** Optional human description for Console / docs (falls back to the JS key). */
|
|
136
|
-
describe(description: string): FieldBuilder
|
|
161
|
+
describe(description: string): FieldBuilder<TData, TNotNull>;
|
|
137
162
|
/**
|
|
138
163
|
* Declare a foreign key to another column (dialect-agnostic).
|
|
139
164
|
*
|
|
140
165
|
* @param ref - Lazy target column (`() => links.code`)
|
|
141
166
|
* @param actions - Optional ON DELETE / ON UPDATE
|
|
142
167
|
*/
|
|
143
|
-
references(
|
|
168
|
+
references(
|
|
169
|
+
ref: () => SchemaColumnDecl,
|
|
170
|
+
actions?: ReferenceActions,
|
|
171
|
+
): FieldBuilder<TData, TNotNull>;
|
|
144
172
|
/**
|
|
145
173
|
* Bind the JS key and produce a {@link SchemaColumnDecl}.
|
|
146
174
|
*
|
|
147
175
|
* @param key - Object key in the table column map
|
|
148
176
|
*/
|
|
149
|
-
finalize(key: string): SchemaColumnDecl
|
|
177
|
+
finalize(key: string): SchemaColumnDecl<TData, TNotNull>;
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
interface FieldState {
|
|
@@ -186,31 +214,39 @@ function mergeClassification(
|
|
|
186
214
|
return { ...(current ?? {}), ...patch };
|
|
187
215
|
}
|
|
188
216
|
|
|
189
|
-
function createBuilder
|
|
190
|
-
|
|
217
|
+
function createBuilder<TData, TNotNull extends boolean>(
|
|
218
|
+
state: FieldState,
|
|
219
|
+
): FieldBuilder<TData, TNotNull> {
|
|
220
|
+
const next = <N extends boolean>(patch: Partial<FieldState>): FieldBuilder<TData, N> =>
|
|
221
|
+
createBuilder<TData, N>({ ...state, ...patch });
|
|
191
222
|
|
|
192
223
|
return {
|
|
193
|
-
primaryKey: () => next({ primaryKey: true, notNull: true }),
|
|
194
|
-
notNull: () => next({ notNull: true }),
|
|
195
|
-
unique: () => next({ unique: true }),
|
|
196
|
-
default: (value) => next({ defaultValue: value }),
|
|
224
|
+
primaryKey: () => next<true>({ primaryKey: true, notNull: true }),
|
|
225
|
+
notNull: () => next<true>({ notNull: true }),
|
|
226
|
+
unique: () => next<TNotNull>({ unique: true }),
|
|
227
|
+
default: (value) => next<TNotNull>({ defaultValue: value }),
|
|
197
228
|
defaultFn: (fn) =>
|
|
198
|
-
next({
|
|
229
|
+
next<TNotNull>({
|
|
199
230
|
defaultFn: fn,
|
|
200
231
|
defaultFnKind: defaultFnKindOf(fn),
|
|
201
232
|
}),
|
|
202
|
-
pii: () =>
|
|
233
|
+
pii: () =>
|
|
234
|
+
next<TNotNull>({ classification: mergeClassification(state.classification, { pii: true }) }),
|
|
203
235
|
sensitive: () =>
|
|
204
|
-
next({
|
|
236
|
+
next<TNotNull>({
|
|
237
|
+
classification: mergeClassification(state.classification, { sensitive: true }),
|
|
238
|
+
}),
|
|
205
239
|
retain: (duration) =>
|
|
206
|
-
next({
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
next<TNotNull>({
|
|
241
|
+
classification: mergeClassification(state.classification, { retain: duration }),
|
|
242
|
+
}),
|
|
243
|
+
as: (sqlName) => next<TNotNull>({ sqlName }),
|
|
244
|
+
describe: (description) => next<TNotNull>({ description }),
|
|
209
245
|
references: (ref, actions) =>
|
|
210
|
-
next({
|
|
246
|
+
next<TNotNull>({
|
|
211
247
|
references: actions ? { ref, actions } : { ref },
|
|
212
248
|
}),
|
|
213
|
-
finalize(key: string): SchemaColumnDecl {
|
|
249
|
+
finalize(key: string): SchemaColumnDecl<TData, TNotNull> {
|
|
214
250
|
const sqlName = state.sqlName ?? camelToSnake(key);
|
|
215
251
|
return {
|
|
216
252
|
key,
|
|
@@ -218,7 +254,7 @@ function createBuilder(state: FieldState): FieldBuilder {
|
|
|
218
254
|
sqlName,
|
|
219
255
|
sqlType: state.sqlType,
|
|
220
256
|
primaryKey: state.primaryKey,
|
|
221
|
-
notNull: state.notNull || state.primaryKey,
|
|
257
|
+
notNull: (state.notNull || state.primaryKey) as TNotNull,
|
|
222
258
|
unique: state.unique,
|
|
223
259
|
...(state.defaultValue !== undefined ? { defaultValue: state.defaultValue } : {}),
|
|
224
260
|
...(state.defaultFn ? { defaultFn: state.defaultFn } : {}),
|
|
@@ -240,15 +276,15 @@ function createBuilder(state: FieldState): FieldBuilder {
|
|
|
240
276
|
* Field builders — `field.text()` / `field.integer()`.
|
|
241
277
|
*/
|
|
242
278
|
export const field = {
|
|
243
|
-
text: (): FieldBuilder =>
|
|
244
|
-
createBuilder({
|
|
279
|
+
text: (): FieldBuilder<string, false> =>
|
|
280
|
+
createBuilder<string, false>({
|
|
245
281
|
sqlType: "text",
|
|
246
282
|
primaryKey: false,
|
|
247
283
|
notNull: false,
|
|
248
284
|
unique: false,
|
|
249
285
|
}),
|
|
250
|
-
integer: (): FieldBuilder =>
|
|
251
|
-
createBuilder({
|
|
286
|
+
integer: (): FieldBuilder<number, false> =>
|
|
287
|
+
createBuilder<number, false>({
|
|
252
288
|
sqlType: "integer",
|
|
253
289
|
primaryKey: false,
|
|
254
290
|
notNull: false,
|
|
@@ -256,8 +292,19 @@ export const field = {
|
|
|
256
292
|
}),
|
|
257
293
|
} as const;
|
|
258
294
|
|
|
259
|
-
/**
|
|
260
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Column map input for {@link schemaTable}.
|
|
297
|
+
*
|
|
298
|
+
* Explicit `string` / `number` × nullability variants so `field.text()`
|
|
299
|
+
* (`FieldBuilder<string, false>`) remains assignable — methodful builders
|
|
300
|
+
* are invariant in their type parameters.
|
|
301
|
+
*/
|
|
302
|
+
export type SchemaColumnInput =
|
|
303
|
+
| FieldBuilder<string, false>
|
|
304
|
+
| FieldBuilder<string, true>
|
|
305
|
+
| FieldBuilder<number, false>
|
|
306
|
+
| FieldBuilder<number, true>
|
|
307
|
+
| SchemaColumnDecl;
|
|
261
308
|
|
|
262
309
|
/**
|
|
263
310
|
* Whether a value is a finalized {@link SchemaColumnDecl}.
|
|
@@ -301,7 +348,7 @@ export function finalizeColumnMap(
|
|
|
301
348
|
): Record<string, SchemaColumnDecl> {
|
|
302
349
|
const out: Record<string, SchemaColumnDecl> = {};
|
|
303
350
|
for (const [key, value] of Object.entries(columns)) {
|
|
304
|
-
out[key] = isFieldBuilder(value) ? value.finalize(key) : value;
|
|
351
|
+
out[key] = isFieldBuilder(value) ? value.finalize(key) : (value as SchemaColumnDecl);
|
|
305
352
|
}
|
|
306
353
|
return out;
|
|
307
354
|
}
|
|
@@ -333,14 +380,34 @@ export function schemaTableSqlName(table: SchemaTableDecl): string | undefined {
|
|
|
333
380
|
return undefined;
|
|
334
381
|
}
|
|
335
382
|
|
|
383
|
+
/**
|
|
384
|
+
* JS value type for a finalized column (`string` / `number`, plus `| null`
|
|
385
|
+
* when the column is nullable).
|
|
386
|
+
*/
|
|
387
|
+
export type InferColumnJs<C> =
|
|
388
|
+
C extends SchemaColumnDecl<infer D, infer N> ? (N extends true ? D : D | null) : unknown;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Finalize a {@link schemaTable} column input into a typed decl.
|
|
392
|
+
*/
|
|
393
|
+
export type FinalizeColumn<T> =
|
|
394
|
+
T extends FieldBuilder<infer D, infer N>
|
|
395
|
+
? SchemaColumnDecl<D, N>
|
|
396
|
+
: T extends SchemaColumnDecl<infer D, infer N>
|
|
397
|
+
? SchemaColumnDecl<D, N>
|
|
398
|
+
: SchemaColumnDecl;
|
|
399
|
+
|
|
336
400
|
/**
|
|
337
401
|
* Table decl with columns as own properties (`links.code`) for FK / relations.
|
|
402
|
+
*
|
|
403
|
+
* `$inferSelect` is type-level only (Drizzle's own alias) — not set at runtime.
|
|
338
404
|
*/
|
|
339
405
|
export type SchemaTableWithColumns<
|
|
340
406
|
C extends Record<string, SchemaColumnDecl> = Record<string, SchemaColumnDecl>,
|
|
341
407
|
> = Omit<SchemaTableDecl, "columns"> & {
|
|
342
408
|
readonly columns: Readonly<C>;
|
|
343
|
-
|
|
409
|
+
readonly $inferSelect: { [K in keyof C]: InferColumnJs<C[K]> };
|
|
410
|
+
} & Omit<C, "kind" | "tableName" | "$inferSelect">;
|
|
344
411
|
|
|
345
412
|
/**
|
|
346
413
|
* Declare an abstract schema table (ORM-agnostic).
|
|
@@ -356,7 +423,7 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
|
|
|
356
423
|
name: string,
|
|
357
424
|
columns: C,
|
|
358
425
|
extras: readonly SchemaTableExtra[] = [],
|
|
359
|
-
): SchemaTableWithColumns<{ [K in keyof C]:
|
|
426
|
+
): SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }> {
|
|
360
427
|
const finalized = finalizeColumnMap(columns);
|
|
361
428
|
const stamped: Record<string, SchemaColumnDecl> = {};
|
|
362
429
|
for (const [key, col] of Object.entries(finalized)) {
|
|
@@ -366,18 +433,22 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
|
|
|
366
433
|
(extra): extra is SchemaPolicyDecl => extra.kind === "schema-policy",
|
|
367
434
|
);
|
|
368
435
|
const rls = extras.some((extra) => extra.kind === "schema-rls") || policies.length > 0;
|
|
436
|
+
const unscoped = extras.some(
|
|
437
|
+
(extra) => extra.kind === "schema-tenant-scoped" && extra.tenantScoped === false,
|
|
438
|
+
);
|
|
369
439
|
const table = {
|
|
370
440
|
name,
|
|
371
441
|
columns: stamped,
|
|
372
442
|
...stamped,
|
|
373
443
|
...(rls ? { rls: true } : {}),
|
|
374
444
|
...(policies.length > 0 ? { policies } : {}),
|
|
445
|
+
...(unscoped ? { tenantScoped: false } : {}),
|
|
375
446
|
};
|
|
376
447
|
// Survive columns named `name` or `kind` (they would otherwise shadow
|
|
377
448
|
// the table discriminant / SQL name).
|
|
378
449
|
Object.defineProperty(table, "kind", { value: "schema-table", enumerable: false });
|
|
379
450
|
Object.defineProperty(table, "tableName", { value: name, enumerable: false });
|
|
380
|
-
return table as SchemaTableWithColumns<{ [K in keyof C]:
|
|
451
|
+
return table as SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }>;
|
|
381
452
|
}
|
|
382
453
|
|
|
383
454
|
/**
|
|
@@ -409,12 +480,25 @@ function sqlStringLiteral(value: string): string {
|
|
|
409
480
|
return `'${value.replaceAll("'", "''")}'`;
|
|
410
481
|
}
|
|
411
482
|
|
|
412
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Policy name `prefix_key_command`.
|
|
485
|
+
*
|
|
486
|
+
* @param prefix - Helper kind
|
|
487
|
+
* @param key - Gate / column / scope
|
|
488
|
+
* @param command - SQL command
|
|
489
|
+
*/
|
|
490
|
+
export function helperPolicyName(prefix: string, key: string, command: SchemaPolicyFor): string {
|
|
413
491
|
const slug = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
414
492
|
return `${prefix}_${slug}_${command}`;
|
|
415
493
|
}
|
|
416
494
|
|
|
417
|
-
|
|
495
|
+
/**
|
|
496
|
+
* USING / WITH CHECK split for a helper expression.
|
|
497
|
+
*
|
|
498
|
+
* @param command - SQL command
|
|
499
|
+
* @param expr - Predicate
|
|
500
|
+
*/
|
|
501
|
+
export function policyPredicates(
|
|
418
502
|
command: SchemaPolicyFor,
|
|
419
503
|
expr: string,
|
|
420
504
|
): { readonly using?: string; readonly withCheck?: string } {
|
|
@@ -482,13 +566,46 @@ export type SchemaPolicyApi = typeof schemaPolicy & {
|
|
|
482
566
|
readonly gate: typeof schemaPolicyGate;
|
|
483
567
|
readonly owner: typeof schemaPolicyOwner;
|
|
484
568
|
readonly scope: typeof schemaPolicyScope;
|
|
569
|
+
readonly tenant: (
|
|
570
|
+
column: string,
|
|
571
|
+
options?: Pick<SchemaPolicyOptions, "for" | "as" | "to">,
|
|
572
|
+
) => SchemaPolicyDecl;
|
|
485
573
|
};
|
|
486
574
|
|
|
575
|
+
function loadSchemaTenant(): {
|
|
576
|
+
tenant: SchemaPolicyApi["tenant"];
|
|
577
|
+
unscoped: () => SchemaTenantScopedDecl;
|
|
578
|
+
} {
|
|
579
|
+
return lazyRequire(import.meta.dir, ["schema", "tenant"].join("-"));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Tenant-column policy — `tenant_id = oke.tenant()`.
|
|
584
|
+
*
|
|
585
|
+
* @param column - SQL / JS column name
|
|
586
|
+
* @param options - Command (default `all`)
|
|
587
|
+
*/
|
|
588
|
+
export function schemaPolicyTenant(
|
|
589
|
+
column: string,
|
|
590
|
+
options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
|
|
591
|
+
): SchemaPolicyDecl {
|
|
592
|
+
return loadSchemaTenant().tenant(column, options);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Mark a table as globally shared (`tenantScoped: false`).
|
|
597
|
+
* Required when `gate.auth.tenant` is on and the table has no tenant policy.
|
|
598
|
+
*/
|
|
599
|
+
export function schemaUnscoped(): SchemaTenantScopedDecl {
|
|
600
|
+
return loadSchemaTenant().unscoped();
|
|
601
|
+
}
|
|
602
|
+
|
|
487
603
|
/** `store.schema.policy` — raw + Gate helpers. */
|
|
488
604
|
export const schemaPolicyApi: SchemaPolicyApi = Object.assign(schemaPolicy, {
|
|
489
605
|
gate: schemaPolicyGate,
|
|
490
606
|
owner: schemaPolicyOwner,
|
|
491
607
|
scope: schemaPolicyScope,
|
|
608
|
+
tenant: schemaPolicyTenant,
|
|
492
609
|
});
|
|
493
610
|
|
|
494
611
|
// ─── Relations (mirrors drizzle-orm `defineRelations`) ───────────────────────
|
|
@@ -688,6 +805,7 @@ export const schema = {
|
|
|
688
805
|
relations: schemaRelations,
|
|
689
806
|
rls: schemaRls,
|
|
690
807
|
policy: schemaPolicyApi,
|
|
808
|
+
unscoped: schemaUnscoped,
|
|
691
809
|
} as const;
|
|
692
810
|
|
|
693
811
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant schema helpers — lazy chunk off Store-only `oke()` graphs.
|
|
3
|
+
*
|
|
4
|
+
* `store.schema.policy.tenant` / `store.schema.unscoped` stay on the public
|
|
5
|
+
* API via getters; this module is loaded on first access.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
schemaPolicy,
|
|
10
|
+
helperPolicyName,
|
|
11
|
+
policyPredicates,
|
|
12
|
+
type SchemaPolicyDecl,
|
|
13
|
+
type SchemaPolicyOptions,
|
|
14
|
+
type SchemaTenantScopedDecl,
|
|
15
|
+
} from "./schema-decl.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tenant-column policy — `tenant_id = oke.tenant()`.
|
|
19
|
+
*
|
|
20
|
+
* @param column - SQL / JS column name
|
|
21
|
+
* @param options - Command (default `all`)
|
|
22
|
+
*/
|
|
23
|
+
export function tenant(
|
|
24
|
+
column: string,
|
|
25
|
+
options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
|
|
26
|
+
): SchemaPolicyDecl {
|
|
27
|
+
const command = options.for ?? "all";
|
|
28
|
+
return schemaPolicy(helperPolicyName("tenant", column, command), {
|
|
29
|
+
...options,
|
|
30
|
+
for: command,
|
|
31
|
+
...policyPredicates(command, `${column} = oke.tenant()`),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mark a table as globally shared (`tenantScoped: false`).
|
|
37
|
+
* Required when `gate.auth.tenant` is on and the table has no tenant policy.
|
|
38
|
+
*/
|
|
39
|
+
export function unscoped(): SchemaTenantScopedDecl {
|
|
40
|
+
return { kind: "schema-tenant-scoped", tenantScoped: false };
|
|
41
|
+
}
|
|
@@ -100,6 +100,45 @@ describe("sql-session RLS isolation (pglite)", () => {
|
|
|
100
100
|
{ id: "b", owner: "bob" },
|
|
101
101
|
]);
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
test("tenant_id = oke.tenant() isolates concurrent tenants", async () => {
|
|
105
|
+
await conn.exec(`CREATE TABLE bookings (
|
|
106
|
+
id text PRIMARY KEY,
|
|
107
|
+
tenant_id text NOT NULL,
|
|
108
|
+
body text NOT NULL
|
|
109
|
+
)`);
|
|
110
|
+
await conn.exec(`INSERT INTO bookings VALUES ('1', 'acme', 'a'), ('2', 'globex', 'b')`);
|
|
111
|
+
await conn.exec(`ALTER TABLE bookings ENABLE ROW LEVEL SECURITY`);
|
|
112
|
+
await conn.exec(`GRANT SELECT, INSERT, UPDATE, DELETE ON bookings TO oke_app`);
|
|
113
|
+
await conn.exec(`CREATE POLICY tenant_all ON bookings
|
|
114
|
+
AS PERMISSIVE FOR ALL TO public
|
|
115
|
+
USING (tenant_id = oke.tenant())
|
|
116
|
+
WITH CHECK (tenant_id = oke.tenant())`);
|
|
117
|
+
|
|
118
|
+
const asAcme = createSqlStoreHandle("sql:app", {
|
|
119
|
+
connection: conn,
|
|
120
|
+
classifications: new Map(),
|
|
121
|
+
routedRole: "primary",
|
|
122
|
+
domainDdl: "off",
|
|
123
|
+
rls: { gate: "member", userId: "u1", scopes: ["member"], tenantId: "acme" },
|
|
124
|
+
});
|
|
125
|
+
const asGlobex = createSqlStoreHandle("sql:app", {
|
|
126
|
+
connection: conn,
|
|
127
|
+
classifications: new Map(),
|
|
128
|
+
routedRole: "primary",
|
|
129
|
+
domainDdl: "off",
|
|
130
|
+
rls: { gate: "member", userId: "u1", scopes: ["member"], tenantId: "globex" },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const [acme, globex, acmeAgain] = await Promise.all([
|
|
134
|
+
asAcme.raw(`SELECT id, tenant_id FROM bookings ORDER BY id`),
|
|
135
|
+
asGlobex.raw(`SELECT id, tenant_id FROM bookings ORDER BY id`),
|
|
136
|
+
asAcme.raw(`SELECT id, tenant_id FROM bookings ORDER BY id`),
|
|
137
|
+
]);
|
|
138
|
+
expect(acme).toEqual([{ id: "1", tenant_id: "acme" }]);
|
|
139
|
+
expect(globex).toEqual([{ id: "2", tenant_id: "globex" }]);
|
|
140
|
+
expect(acmeAgain).toEqual([{ id: "1", tenant_id: "acme" }]);
|
|
141
|
+
});
|
|
103
142
|
});
|
|
104
143
|
|
|
105
144
|
const LIVE_PG =
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level proof: `select().from(store.schema.table())` resolves to the
|
|
3
|
+
* declared column JS shape — not `SqlRow`.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import type { SqlRow } from "../../drivers/types.ts";
|
|
8
|
+
import { field, schemaTable } from "./schema-decl.ts";
|
|
9
|
+
import type { InferSelectRow, SqlStoreHandle } from "./sql-session.ts";
|
|
10
|
+
import { defineTable } from "./table.ts";
|
|
11
|
+
|
|
12
|
+
/** Compile-time equality. */
|
|
13
|
+
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
|
|
14
|
+
type Assert<T extends true> = T;
|
|
15
|
+
|
|
16
|
+
const items = schemaTable("items", {
|
|
17
|
+
id: field.text().primaryKey(),
|
|
18
|
+
title: field.text().notNull(),
|
|
19
|
+
note: field.text(),
|
|
20
|
+
count: field.integer().notNull(),
|
|
21
|
+
score: field.integer(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type ExpectedRow = {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
note: string | null;
|
|
28
|
+
count: number;
|
|
29
|
+
score: number | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async function selectAll(db: SqlStoreHandle) {
|
|
33
|
+
return db.select().from(items);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function selectWhere(db: SqlStoreHandle) {
|
|
37
|
+
return db.select().from(items).where({ id: "x" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function selectOrder(db: SqlStoreHandle) {
|
|
41
|
+
return db.select().from(items).orderBy(items.title);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function selectLimit(db: SqlStoreHandle) {
|
|
45
|
+
return db.select().from(items).limit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function selectProj(db: SqlStoreHandle) {
|
|
49
|
+
return db.select({ title: items.title }).from(items);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const bare = defineTable("bare", { id: { name: "id" } });
|
|
53
|
+
|
|
54
|
+
async function selectBare(db: SqlStoreHandle) {
|
|
55
|
+
return db.select().from(bare);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type _Infer = Assert<Eq<InferSelectRow<typeof items>, ExpectedRow>>;
|
|
59
|
+
type _All = Assert<Eq<Awaited<ReturnType<typeof selectAll>>, ExpectedRow[]>>;
|
|
60
|
+
type _Where = Assert<Eq<Awaited<ReturnType<typeof selectWhere>>, ExpectedRow[]>>;
|
|
61
|
+
type _Order = Assert<Eq<Awaited<ReturnType<typeof selectOrder>>, ExpectedRow[]>>;
|
|
62
|
+
type _Limit = Assert<Eq<Awaited<ReturnType<typeof selectLimit>>, ExpectedRow[]>>;
|
|
63
|
+
type _Proj = Assert<Eq<Awaited<ReturnType<typeof selectProj>>, SqlRow[]>>;
|
|
64
|
+
type _Bare = Assert<Eq<Awaited<ReturnType<typeof selectBare>>, SqlRow[]>>;
|
|
65
|
+
|
|
66
|
+
describe("select().from() row inference", () => {
|
|
67
|
+
test("type-level row shape matches declared columns", () => {
|
|
68
|
+
const _keep: _Infer & _All & _Where & _Order & _Limit & _Proj & _Bare = true;
|
|
69
|
+
expect(_keep).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|