Skip to content

Commit 8790769

Browse files
aasonicloutiertylerclockwork-labs-botbfops
authored
Add TypeScript submodule support for SpacetimeDB modules (#5486)
Add TypeScript submodule support for SpacetimeDB modules # Description of Changes - TypeScript server SDK: runtime API for module mounting (submodules), including namespaced dispatch, ctx.as helpers for cross-namespace calls, and HTTP handler delegation - TypeScript client codegen: namespace-aware table/reducer/procedure exports and query builder for submodule tables and views - Migrations: auto-migrate support for submodule tables, views, and indexes; AddConstraint step added alongside NamespacedIdentifier-based migration steps - CLI: namespace support for `spacetime call` and `spacetime sql` - C++/C# bindings: autogenerated RawSubmoduleV10 types - module-test-ts: submodule integration test (lib_submodule) # API and ABI breaking changes Adds a submodule field to ModuleDef. Older modules will be publishable to new servers, but new modules that have submodules will not be publishable to old servers. # Expected complexity level and risk 5 # Testing Beyond the rust tests defined in this PR, the following tests were done on the full PR sequence once the entire namespace feature was implemented for typescript: Feature Test Checklist Module: - [x] root module can import another module and mount it with a namespace under its schema - [x] root module and submodule can have the same function and table names without conflicting - [x] ctx.db.lib.lib_table is readable/writable inside root module reducer - [x] ctx.db.lib.lib_table is readable/writable inside root module procedure withTx block - [x] library_reducer(ctx.as.lib) is callable inside root module reducer Client - [x] Client can subscribe to lib.library_table - [x] Client can subscribe to lib.library_view - [x] Client can call lib/library_reducer - [x] Client can call lib/library_procedure - [x] Client can subscribe to lib.sublib.sublib_table - [x] Client can subscribe to lib.sublib.sublib_view - [x] Client can call lib/sublib/sublib_reducer - [x] Client can call lib/sublib/sublib_procedure CLI - [x] CLI can subscribe to lib.library_table - [x] CLI can subscribe to lib.library_view - [x] CLI can call lib/library_reducer - [x] CLI can call lib/library_procedure - [x] CLI can subscribe to lib.sublib.sublib_table - [x] CLI can subscribe to lib.sublib.sublib_view - [x] CLI can call lib/sublib/sublib_reducer - [x] CLI can call lib/sublib/sublib_procedure Migration - [x] Module migrates without issue from having a submodule to not having a submodule - [x] Module migrates without issue from not having a submodule to having a submodule - [x] Module migrates without issue when having a submodule and root module change occurs (change reducer signature, add table, add column with default, change reducer function body, change index) - [x] Module migrates without issue when having a submodule and a submodule change occurs (change reducer signature, add table, add column with default, change reducer function body, change index) Commit Log - [x] Module loads fine from commit log - [x] Module snapshot is created without issue - [x] Module loads fine from snapshot --------- Co-authored-by: Tyler Cloutier <cloutiertyler@aol.com> Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com> Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
1 parent 8e5510b commit 8790769

90 files changed

Lines changed: 6403 additions & 1312 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,13 @@ jobs:
528528
save-if: false
529529
prefix-key: v1
530530

531+
- name: Set up Node.js
532+
uses: actions/setup-node@v4
533+
with:
534+
node-version: 22
535+
536+
- uses: ./.github/actions/setup-pnpm
537+
531538
- name: Run bindgen tests
532539
run: cargo ci wasm-bindings
533540

crates/bench/src/spacetime_raw.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl BenchDatabase for SpacetimeRaw {
6969
IndexSchema {
7070
index_id: IndexId::SENTINEL,
7171
table_id,
72-
index_name: column.name.clone().unwrap(),
72+
index_name: column.name.clone().unwrap().into(),
7373
index_algorithm: IndexAlgorithm::BTree(BTreeAlgorithm {
7474
columns: ColId(i as _).into(),
7575
}),

crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV10Section.g.h

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,23 @@
1212
#include <memory>
1313
#include "../autogen_base.h"
1414
#include "spacetimedb/bsatn/bsatn.h"
15-
#include "RawViewPrimaryKeyDefV10.g.h"
1615
#include "RawTableDefV10.g.h"
17-
#include "RawViewDefV10.g.h"
18-
#include "RawScheduleDefV10.g.h"
19-
#include "CaseConversionPolicy.g.h"
20-
#include "RawRowLevelSecurityDefV9.g.h"
21-
#include "RawHttpRouteDefV10.g.h"
22-
#include "RawReducerDefV10.g.h"
2316
#include "Typespace.g.h"
2417
#include "RawLifeCycleReducerDefV10.g.h"
25-
#include "ExplicitNames.g.h"
26-
#include "RawHttpHandlerDefV10.g.h"
18+
#include "RawReducerDefV10.g.h"
2719
#include "RawTypeDefV10.g.h"
20+
#include "RawRowLevelSecurityDefV9.g.h"
21+
#include "RawHttpRouteDefV10.g.h"
22+
#include "RawSubmoduleV10.g.h"
23+
#include "RawViewDefV10.g.h"
24+
#include "ExplicitNames.g.h"
2825
#include "RawProcedureDefV10.g.h"
26+
#include "CaseConversionPolicy.g.h"
27+
#include "RawScheduleDefV10.g.h"
28+
#include "RawViewPrimaryKeyDefV10.g.h"
29+
#include "RawHttpHandlerDefV10.g.h"
2930

3031
namespace SpacetimeDB::Internal {
3132

32-
SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector<SpacetimeDB::Internal::RawTypeDefV10>, std::vector<SpacetimeDB::Internal::RawTableDefV10>, std::vector<SpacetimeDB::Internal::RawReducerDefV10>, std::vector<SpacetimeDB::Internal::RawProcedureDefV10>, std::vector<SpacetimeDB::Internal::RawViewDefV10>, std::vector<SpacetimeDB::Internal::RawScheduleDefV10>, std::vector<SpacetimeDB::Internal::RawLifeCycleReducerDefV10>, std::vector<SpacetimeDB::Internal::RawRowLevelSecurityDefV9>, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames, std::vector<SpacetimeDB::Internal::RawHttpHandlerDefV10>, std::vector<SpacetimeDB::Internal::RawHttpRouteDefV10>, std::vector<SpacetimeDB::Internal::RawViewPrimaryKeyDefV10>)
33+
SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector<SpacetimeDB::Internal::RawTypeDefV10>, std::vector<SpacetimeDB::Internal::RawTableDefV10>, std::vector<SpacetimeDB::Internal::RawReducerDefV10>, std::vector<SpacetimeDB::Internal::RawProcedureDefV10>, std::vector<SpacetimeDB::Internal::RawViewDefV10>, std::vector<SpacetimeDB::Internal::RawScheduleDefV10>, std::vector<SpacetimeDB::Internal::RawLifeCycleReducerDefV10>, std::vector<SpacetimeDB::Internal::RawRowLevelSecurityDefV9>, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames, std::vector<SpacetimeDB::Internal::RawHttpHandlerDefV10>, std::vector<SpacetimeDB::Internal::RawHttpRouteDefV10>, std::vector<SpacetimeDB::Internal::RawViewPrimaryKeyDefV10>, std::vector<SpacetimeDB::Internal::RawSubmoduleV10>)
3334
} // namespace SpacetimeDB::Internal
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
2+
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
3+
4+
// This was generated using spacetimedb codegen.
5+
6+
#pragma once
7+
8+
#include <cstdint>
9+
#include <string>
10+
#include <vector>
11+
#include <optional>
12+
#include <memory>
13+
#include "../autogen_base.h"
14+
#include "spacetimedb/bsatn/bsatn.h"
15+
16+
namespace SpacetimeDB::Internal {
17+
struct RawModuleDefV10;
18+
} // namespace SpacetimeDB::Internal
19+
20+
namespace SpacetimeDB::Internal {
21+
22+
SPACETIMEDB_INTERNAL_PRODUCT_TYPE(RawSubmoduleV10) {
23+
std::string namespace_;
24+
std::shared_ptr<SpacetimeDB::Internal::RawModuleDefV10> module;
25+
26+
void bsatn_serialize(::SpacetimeDB::bsatn::Writer& writer) const {
27+
::SpacetimeDB::bsatn::serialize(writer, namespace_);
28+
::SpacetimeDB::bsatn::serialize(writer, *module);
29+
}
30+
SPACETIMEDB_PRODUCT_TYPE_EQUALITY(namespace_, module)
31+
};
32+
} // namespace SpacetimeDB::Internal

crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-csharp/Runtime/Internal/Autogen/RawSubmoduleV10.g.cs

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-typescript/src/lib/autogen/types.ts

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-typescript/src/lib/query.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ConnectionId } from './connection_id';
22
import { Identity } from './identity';
33
import type { ColumnIndex, IndexColumns, IndexOpts } from './indexes';
44
import type { UntypedSchemaDef } from './schema';
5+
import type { UntypedTableDef } from './table';
56
import type { UntypedTableSchema } from './table_schema';
67
import { Timestamp } from './timestamp';
78
import type {
@@ -223,6 +224,26 @@ export type QueryBuilder<SchemaDef extends UntypedSchemaDef> = {
223224
> as Tbl['accessorName']]: TableRef<Tbl> & From<Tbl>;
224225
} & {};
225226

227+
/**
228+
* Like `QueryBuilder`, but with declared namespaces also exposed as sub-objects.
229+
* This is the type of the `tables` argument in a `subscribe` query-builder callback.
230+
*
231+
* Root-level tables appear as direct properties (same as `QueryBuilder`).
232+
* Declared namespaces appear as sub-objects — each is itself a `QueryBuilder` for that
233+
* namespace's schema, so `tables.<namespace>.<table>` is fully typed.
234+
*
235+
* When `SchemaDef['namespaces']` is absent or `{}`, no namespace properties appear —
236+
* accessing an undeclared namespace is a compile error.
237+
*/
238+
export type NamespacedQueryBuilder<SchemaDef extends UntypedSchemaDef> =
239+
QueryBuilder<SchemaDef> & {
240+
readonly [NS in keyof NonNullable<SchemaDef['namespaces']>]: NonNullable<
241+
SchemaDef['namespaces']
242+
>[NS] extends UntypedSchemaDef
243+
? QueryBuilder<NonNullable<SchemaDef['namespaces']>[NS]>
244+
: never;
245+
};
246+
226247
/**
227248
* A runtime reference to a table. This materializes the RowExpr for us.
228249
* TODO: Maybe add the full SchemaDef to the type signature depending on how joins will work.
@@ -335,6 +356,42 @@ export function makeQueryBuilder<SchemaDef extends UntypedSchemaDef>(
335356
return Object.freeze(qb) as QueryBuilder<SchemaDef>;
336357
}
337358

359+
/**
360+
* Builds the namespace-aware `tables` object passed to `subscribe`'s query-builder callback.
361+
*
362+
* Tables whose `sourceName` contains no `.` are placed at the root.
363+
* Tables with a dotted `sourceName` (e.g. `"namespace.table"`) are grouped under a
364+
* sub-object keyed by the namespace alias, with the part after the dot as the
365+
* property key within that namespace.
366+
*/
367+
export function makeFromBuilder<SchemaDef extends UntypedSchemaDef>(
368+
tables: SchemaDef['tables']
369+
): NamespacedQueryBuilder<SchemaDef> {
370+
const result: Record<string, unknown> = Object.create(null);
371+
const namespaces: Record<string, Record<string, unknown>> = Object.create(
372+
null
373+
);
374+
375+
for (const table of Object.values(tables) as UntypedTableDef[]) {
376+
const dotIdx = table.sourceName.indexOf('.');
377+
if (dotIdx === -1) {
378+
result[table.accessorName] = createTableRefFromDef(table as any);
379+
} else {
380+
const ns = table.sourceName.slice(0, dotIdx);
381+
const key = table.sourceName.slice(dotIdx + 1);
382+
(namespaces[ns] ??= Object.create(null))[key] = createTableRefFromDef(
383+
table as any
384+
);
385+
}
386+
}
387+
388+
for (const [ns, nsTables] of Object.entries(namespaces)) {
389+
result[ns] = Object.freeze(nsTables);
390+
}
391+
392+
return Object.freeze(result) as unknown as NamespacedQueryBuilder<SchemaDef>;
393+
}
394+
338395
function createRowExpr<TableDef extends TypedTableDef>(
339396
tableDef: TableDef
340397
): RowExpr<TableDef> {
@@ -879,7 +936,10 @@ function literalValueToSql(value: unknown): string {
879936
}
880937

881938
function quoteIdentifier(name: string): string {
882-
return `"${name.replace(/"/g, '""')}"`;
939+
return name
940+
.split('.')
941+
.map(part => `"${part.replace(/"/g, '""')}"`)
942+
.join('.');
883943
}
884944

885945
function isLiteralExpr<Value>(

crates/bindings-typescript/src/lib/reducers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ export interface JwtClaims {
9898
readonly fullPayload: JsonObject;
9999
}
100100

101+
export type AliasViews<SchemaDef extends UntypedSchemaDef> = SchemaDef extends {
102+
namespaces: infer NS extends Record<string, UntypedSchemaDef>;
103+
}
104+
? { readonly [K in keyof NS]: ReducerCtx<NS[K]> }
105+
: {};
106+
101107
/**
102108
* Reducer context parametrized by the inferred Schema
103109
*/
@@ -113,4 +119,5 @@ export type ReducerCtx<SchemaDef extends UntypedSchemaDef> = Readonly<{
113119
newUuidV4(): Uuid;
114120
newUuidV7(): Uuid;
115121
random: Random;
122+
as: AliasViews<SchemaDef>;
116123
}>;

crates/bindings-typescript/src/lib/schema.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from './algebraic_type';
88
import type {
99
CaseConversionPolicy,
10+
RawSubmoduleV10,
1011
RawModuleDefV10,
1112
RawModuleDefV10Section,
1213
RawScopedTypeNameV10,
@@ -42,6 +43,7 @@ export type TableNamesOf<S extends UntypedSchemaDef> = Values<
4243
*/
4344
export type UntypedSchemaDef = {
4445
tables: Record<string, UntypedTableDef>;
46+
namespaces?: Record<string, UntypedSchemaDef>;
4547
};
4648

4749
/**
@@ -202,6 +204,7 @@ export class ModuleContext {
202204
explicitNames: {
203205
entries: [],
204206
},
207+
submodules: [],
205208
};
206209

207210
get moduleDef(): ModuleDef {
@@ -266,9 +269,19 @@ export class ModuleContext {
266269
value: module.caseConversionPolicy,
267270
}
268271
);
272+
push(
273+
module.submodules && {
274+
tag: 'Submodules',
275+
value: module.submodules,
276+
}
277+
);
269278
return { sections };
270279
}
271280

281+
addSubmodule(submodule: RawSubmoduleV10) {
282+
this.#moduleDef.submodules.push(submodule);
283+
}
284+
272285
/**
273286
* Set the case conversion policy for this module.
274287
* Called by the settings mechanism.

0 commit comments

Comments
 (0)