diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawMiscModuleExportV9.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawMiscModuleExportV9.g.h index b8e391591df..494243dd470 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawMiscModuleExportV9.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawMiscModuleExportV9.g.h @@ -12,9 +12,9 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" -#include "RawViewDefV9.g.h" #include "RawColumnDefaultValueV9.g.h" #include "RawProcedureDefV9.g.h" +#include "RawViewDefV9.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDef.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDef.g.h index c7f144eb07d..294169acc3b 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDef.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDef.g.h @@ -12,9 +12,9 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" +#include "RawModuleDefV10.g.h" #include "RawModuleDefV8.g.h" #include "RawModuleDefV9.g.h" -#include "RawModuleDefV10.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV10Section.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV10Section.g.h index 241466f467c..1d7cd3ed937 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV10Section.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV10Section.g.h @@ -13,18 +13,19 @@ #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" #include "RawViewDefV10.g.h" -#include "CaseConversionPolicy.g.h" -#include "RawScheduleDefV10.g.h" -#include "RawTableDefV10.g.h" -#include "Typespace.g.h" -#include "RawReducerDefV10.g.h" -#include "RawProcedureDefV10.g.h" #include "RawTypeDefV10.g.h" -#include "RawLifeCycleReducerDefV10.g.h" #include "RawRowLevelSecurityDefV9.g.h" +#include "Typespace.g.h" #include "ExplicitNames.g.h" +#include "RawProcedureDefV10.g.h" +#include "RawModuleMountV10.g.h" +#include "RawScheduleDefV10.g.h" +#include "RawTableDefV10.g.h" +#include "CaseConversionPolicy.g.h" +#include "RawLifeCycleReducerDefV10.g.h" +#include "RawReducerDefV10.g.h" namespace SpacetimeDB::Internal { -SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames) +SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames, std::vector) } // namespace SpacetimeDB::Internal diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV8.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV8.g.h index e856af0fec5..f094166b6e5 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV8.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV8.g.h @@ -12,9 +12,9 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" -#include "ReducerDef.g.h" -#include "MiscModuleExport.g.h" #include "Typespace.g.h" +#include "MiscModuleExport.g.h" +#include "ReducerDef.g.h" #include "TableDesc.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV9.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV9.g.h index 9ab21147e08..1f6406e6126 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV9.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV9.g.h @@ -12,12 +12,12 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" -#include "RawTableDefV9.g.h" -#include "RawTypeDefV9.g.h" -#include "RawMiscModuleExportV9.g.h" #include "Typespace.g.h" #include "RawReducerDefV9.g.h" #include "RawRowLevelSecurityDefV9.g.h" +#include "RawTableDefV9.g.h" +#include "RawTypeDefV9.g.h" +#include "RawMiscModuleExportV9.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleMountV10.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleMountV10.g.h new file mode 100644 index 00000000000..26ac7c9a690 --- /dev/null +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleMountV10.g.h @@ -0,0 +1,37 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +// This was generated using spacetimedb codegen. + +#pragma once + +#include +#include +#include +#include +#include +#include "../autogen_base.h" +#include "spacetimedb/bsatn/bsatn.h" + +// Forward declaration breaks the circular include chain: +// RawModuleMountV10 -> RawModuleDefV10 -> RawModuleDefV10Section -> RawModuleMountV10 +namespace SpacetimeDB::Internal { struct RawModuleDefV10; } + +namespace SpacetimeDB::Internal { + +SPACETIMEDB_INTERNAL_PRODUCT_TYPE(RawModuleMountV10) { + std::string namespace_; // renamed: 'namespace' is a C++ keyword + std::shared_ptr module; // shared_ptr breaks infinite-size recursion + + void bsatn_serialize(::SpacetimeDB::bsatn::Writer& writer) const { + ::SpacetimeDB::bsatn::serialize(writer, namespace_); + if (module) ::SpacetimeDB::bsatn::serialize(writer, *module); + } + bool operator==(const RawModuleMountV10& o) const noexcept { + if (namespace_ != o.namespace_) return false; + if (module && o.module) return *module == *o.module; + return !module && !o.module; + } + bool operator!=(const RawModuleMountV10& o) const noexcept { return !(*this == o); } +}; +} // namespace SpacetimeDB::Internal diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawProcedureDefV10.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawProcedureDefV10.g.h index f316264fc5c..880c2abca08 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawProcedureDefV10.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawProcedureDefV10.g.h @@ -12,9 +12,9 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" -#include "FunctionVisibility.g.h" #include "ProductType.g.h" #include "AlgebraicType.g.h" +#include "FunctionVisibility.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV10.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV10.g.h index 89934c2d4d7..c2ea7a04c30 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV10.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV10.g.h @@ -12,9 +12,9 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" -#include "AlgebraicType.g.h" -#include "FunctionVisibility.g.h" #include "ProductType.g.h" +#include "FunctionVisibility.g.h" +#include "AlgebraicType.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV9.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV9.g.h index 8121773a40d..964ed98df12 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV9.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawReducerDefV9.g.h @@ -12,8 +12,8 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" -#include "Lifecycle.g.h" #include "ProductType.g.h" +#include "Lifecycle.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV10.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV10.g.h index 715364b13cf..1f990422f79 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV10.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV10.g.h @@ -12,12 +12,12 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" -#include "RawIndexDefV10.g.h" #include "TableType.g.h" #include "TableAccess.g.h" #include "RawColumnDefaultValueV10.g.h" -#include "RawConstraintDefV10.g.h" #include "RawSequenceDefV10.g.h" +#include "RawConstraintDefV10.g.h" +#include "RawIndexDefV10.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV8.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV8.g.h index a985ad2f6e7..5f8a53e1be4 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV8.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV8.g.h @@ -12,10 +12,10 @@ #include #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" +#include "RawIndexDefV8.g.h" #include "RawSequenceDefV8.g.h" #include "RawColumnDefV8.g.h" #include "RawConstraintDefV8.g.h" -#include "RawIndexDefV8.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV9.g.h b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV9.g.h index a69a502fb0e..f08563bc809 100644 --- a/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV9.g.h +++ b/crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV9.g.h @@ -13,11 +13,11 @@ #include "../autogen_base.h" #include "spacetimedb/bsatn/bsatn.h" #include "RawScheduleDefV9.g.h" -#include "RawSequenceDefV9.g.h" +#include "RawIndexDefV9.g.h" #include "TableType.g.h" +#include "RawSequenceDefV9.g.h" #include "TableAccess.g.h" #include "RawConstraintDefV9.g.h" -#include "RawIndexDefV9.g.h" namespace SpacetimeDB::Internal { diff --git a/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs b/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs index 1a299f93c3a..e7500844198 100644 --- a/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs +++ b/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs @@ -19,6 +19,7 @@ public partial record RawModuleDefV10Section : SpacetimeDB.TaggedEnum<( System.Collections.Generic.List LifeCycleReducers, System.Collections.Generic.List RowLevelSecurity, SpacetimeDB.CaseConversionPolicy CaseConversionPolicy, - ExplicitNames ExplicitNames + ExplicitNames ExplicitNames, + System.Collections.Generic.List Mounts )>; } diff --git a/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleMountV10.g.cs b/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleMountV10.g.cs new file mode 100644 index 00000000000..0df52895d65 --- /dev/null +++ b/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleMountV10.g.cs @@ -0,0 +1,36 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Internal +{ + [SpacetimeDB.Type] + [DataContract] + public sealed partial class RawModuleMountV10 + { + [DataMember(Name = "namespace")] + public string Namespace; + [DataMember(Name = "module")] + public RawModuleDefV10 Module; + + public RawModuleMountV10( + string Namespace, + RawModuleDefV10 Module + ) + { + this.Namespace = Namespace; + this.Module = Module; + } + + public RawModuleMountV10() + { + this.Namespace = ""; + this.Module = new(); + } + } +} diff --git a/crates/bindings-typescript/src/lib/autogen/types.ts b/crates/bindings-typescript/src/lib/autogen/types.ts index 0ce535eca0f..72ea21fcd5a 100644 --- a/crates/bindings-typescript/src/lib/autogen/types.ts +++ b/crates/bindings-typescript/src/lib/autogen/types.ts @@ -8,13 +8,10 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from '../../lib/type_builders'; +} from "../../lib/type_builders"; // The tagged union or sum type for the algebraic type `AlgebraicType`. -export const AlgebraicType: __TypeBuilder< - __AlgebraicTypeType, - __AlgebraicTypeType -> = __t.enum('AlgebraicType', { +export const AlgebraicType: __TypeBuilder<__AlgebraicTypeType, __AlgebraicTypeType> = __t.enum("AlgebraicType", { Ref: __t.u32(), get Sum() { return SumType; @@ -45,14 +42,14 @@ export const AlgebraicType: __TypeBuilder< export type AlgebraicType = __Infer; // The tagged union or sum type for the algebraic type `CaseConversionPolicy`. -export const CaseConversionPolicy = __t.enum('CaseConversionPolicy', { +export const CaseConversionPolicy = __t.enum("CaseConversionPolicy", { None: __t.unit(), SnakeCase: __t.unit(), }); export type CaseConversionPolicy = __Infer; // The tagged union or sum type for the algebraic type `ExplicitNameEntry`. -export const ExplicitNameEntry = __t.enum('ExplicitNameEntry', { +export const ExplicitNameEntry = __t.enum("ExplicitNameEntry", { get Table() { return NameMapping; }, @@ -65,7 +62,7 @@ export const ExplicitNameEntry = __t.enum('ExplicitNameEntry', { }); export type ExplicitNameEntry = __Infer; -export const ExplicitNames = __t.object('ExplicitNames', { +export const ExplicitNames = __t.object("ExplicitNames", { get entries() { return __t.array(ExplicitNameEntry); }, @@ -73,19 +70,19 @@ export const ExplicitNames = __t.object('ExplicitNames', { export type ExplicitNames = __Infer; // The tagged union or sum type for the algebraic type `FunctionVisibility`. -export const FunctionVisibility = __t.enum('FunctionVisibility', { +export const FunctionVisibility = __t.enum("FunctionVisibility", { Private: __t.unit(), ClientCallable: __t.unit(), }); export type FunctionVisibility = __Infer; -export const HttpHeaderPair = __t.object('HttpHeaderPair', { +export const HttpHeaderPair = __t.object("HttpHeaderPair", { name: __t.string(), value: __t.byteArray(), }); export type HttpHeaderPair = __Infer; -export const HttpHeaders = __t.object('HttpHeaders', { +export const HttpHeaders = __t.object("HttpHeaders", { get entries() { return __t.array(HttpHeaderPair); }, @@ -93,7 +90,7 @@ export const HttpHeaders = __t.object('HttpHeaders', { export type HttpHeaders = __Infer; // The tagged union or sum type for the algebraic type `HttpMethod`. -export const HttpMethod = __t.enum('HttpMethod', { +export const HttpMethod = __t.enum("HttpMethod", { Get: __t.unit(), Head: __t.unit(), Post: __t.unit(), @@ -107,7 +104,7 @@ export const HttpMethod = __t.enum('HttpMethod', { }); export type HttpMethod = __Infer; -export const HttpRequest = __t.object('HttpRequest', { +export const HttpRequest = __t.object("HttpRequest", { get method() { return HttpMethod; }, @@ -122,7 +119,7 @@ export const HttpRequest = __t.object('HttpRequest', { }); export type HttpRequest = __Infer; -export const HttpResponse = __t.object('HttpResponse', { +export const HttpResponse = __t.object("HttpResponse", { get headers() { return HttpHeaders; }, @@ -134,7 +131,7 @@ export const HttpResponse = __t.object('HttpResponse', { export type HttpResponse = __Infer; // The tagged union or sum type for the algebraic type `HttpVersion`. -export const HttpVersion = __t.enum('HttpVersion', { +export const HttpVersion = __t.enum("HttpVersion", { Http09: __t.unit(), Http10: __t.unit(), Http11: __t.unit(), @@ -144,14 +141,14 @@ export const HttpVersion = __t.enum('HttpVersion', { export type HttpVersion = __Infer; // The tagged union or sum type for the algebraic type `IndexType`. -export const IndexType = __t.enum('IndexType', { +export const IndexType = __t.enum("IndexType", { BTree: __t.unit(), Hash: __t.unit(), }); export type IndexType = __Infer; // The tagged union or sum type for the algebraic type `Lifecycle`. -export const Lifecycle = __t.enum('Lifecycle', { +export const Lifecycle = __t.enum("Lifecycle", { Init: __t.unit(), OnConnect: __t.unit(), OnDisconnect: __t.unit(), @@ -159,27 +156,27 @@ export const Lifecycle = __t.enum('Lifecycle', { export type Lifecycle = __Infer; // The tagged union or sum type for the algebraic type `MiscModuleExport`. -export const MiscModuleExport = __t.enum('MiscModuleExport', { +export const MiscModuleExport = __t.enum("MiscModuleExport", { get TypeAlias() { return TypeAlias; }, }); export type MiscModuleExport = __Infer; -export const NameMapping = __t.object('NameMapping', { +export const NameMapping = __t.object("NameMapping", { sourceName: __t.string(), canonicalName: __t.string(), }); export type NameMapping = __Infer; -export const ProductType = __t.object('ProductType', { +export const ProductType = __t.object("ProductType", { get elements() { return __t.array(ProductTypeElement); }, }); export type ProductType = __Infer; -export const ProductTypeElement = __t.object('ProductTypeElement', { +export const ProductTypeElement = __t.object("ProductTypeElement", { name: __t.option(__t.string()), get algebraicType() { return AlgebraicType; @@ -187,7 +184,7 @@ export const ProductTypeElement = __t.object('ProductTypeElement', { }); export type ProductTypeElement = __Infer; -export const RawColumnDefV8 = __t.object('RawColumnDefV8', { +export const RawColumnDefV8 = __t.object("RawColumnDefV8", { colName: __t.string(), get colType() { return AlgebraicType; @@ -195,13 +192,13 @@ export const RawColumnDefV8 = __t.object('RawColumnDefV8', { }); export type RawColumnDefV8 = __Infer; -export const RawColumnDefaultValueV10 = __t.object('RawColumnDefaultValueV10', { +export const RawColumnDefaultValueV10 = __t.object("RawColumnDefaultValueV10", { colId: __t.u16(), value: __t.byteArray(), }); export type RawColumnDefaultValueV10 = __Infer; -export const RawColumnDefaultValueV9 = __t.object('RawColumnDefaultValueV9', { +export const RawColumnDefaultValueV9 = __t.object("RawColumnDefaultValueV9", { table: __t.string(), colId: __t.u16(), value: __t.byteArray(), @@ -209,14 +206,14 @@ export const RawColumnDefaultValueV9 = __t.object('RawColumnDefaultValueV9', { export type RawColumnDefaultValueV9 = __Infer; // The tagged union or sum type for the algebraic type `RawConstraintDataV9`. -export const RawConstraintDataV9 = __t.enum('RawConstraintDataV9', { +export const RawConstraintDataV9 = __t.enum("RawConstraintDataV9", { get Unique() { return RawUniqueConstraintDataV9; }, }); export type RawConstraintDataV9 = __Infer; -export const RawConstraintDefV10 = __t.object('RawConstraintDefV10', { +export const RawConstraintDefV10 = __t.object("RawConstraintDefV10", { sourceName: __t.option(__t.string()), get data() { return RawConstraintDataV9; @@ -224,14 +221,14 @@ export const RawConstraintDefV10 = __t.object('RawConstraintDefV10', { }); export type RawConstraintDefV10 = __Infer; -export const RawConstraintDefV8 = __t.object('RawConstraintDefV8', { +export const RawConstraintDefV8 = __t.object("RawConstraintDefV8", { constraintName: __t.string(), constraints: __t.u8(), columns: __t.array(__t.u16()), }); export type RawConstraintDefV8 = __Infer; -export const RawConstraintDefV9 = __t.object('RawConstraintDefV9', { +export const RawConstraintDefV9 = __t.object("RawConstraintDefV9", { name: __t.option(__t.string()), get data() { return RawConstraintDataV9; @@ -240,14 +237,14 @@ export const RawConstraintDefV9 = __t.object('RawConstraintDefV9', { export type RawConstraintDefV9 = __Infer; // The tagged union or sum type for the algebraic type `RawIndexAlgorithm`. -export const RawIndexAlgorithm = __t.enum('RawIndexAlgorithm', { +export const RawIndexAlgorithm = __t.enum("RawIndexAlgorithm", { BTree: __t.array(__t.u16()), Hash: __t.array(__t.u16()), Direct: __t.u16(), }); export type RawIndexAlgorithm = __Infer; -export const RawIndexDefV10 = __t.object('RawIndexDefV10', { +export const RawIndexDefV10 = __t.object("RawIndexDefV10", { sourceName: __t.option(__t.string()), accessorName: __t.option(__t.string()), get algorithm() { @@ -256,7 +253,7 @@ export const RawIndexDefV10 = __t.object('RawIndexDefV10', { }); export type RawIndexDefV10 = __Infer; -export const RawIndexDefV8 = __t.object('RawIndexDefV8', { +export const RawIndexDefV8 = __t.object("RawIndexDefV8", { indexName: __t.string(), isUnique: __t.bool(), get indexType() { @@ -266,7 +263,7 @@ export const RawIndexDefV8 = __t.object('RawIndexDefV8', { }); export type RawIndexDefV8 = __Infer; -export const RawIndexDefV9 = __t.object('RawIndexDefV9', { +export const RawIndexDefV9 = __t.object("RawIndexDefV9", { name: __t.option(__t.string()), accessorName: __t.option(__t.string()), get algorithm() { @@ -275,21 +272,16 @@ export const RawIndexDefV9 = __t.object('RawIndexDefV9', { }); export type RawIndexDefV9 = __Infer; -export const RawLifeCycleReducerDefV10 = __t.object( - 'RawLifeCycleReducerDefV10', - { - get lifecycleSpec() { - return Lifecycle; - }, - functionName: __t.string(), - } -); -export type RawLifeCycleReducerDefV10 = __Infer< - typeof RawLifeCycleReducerDefV10 ->; +export const RawLifeCycleReducerDefV10 = __t.object("RawLifeCycleReducerDefV10", { + get lifecycleSpec() { + return Lifecycle; + }, + functionName: __t.string(), +}); +export type RawLifeCycleReducerDefV10 = __Infer; // The tagged union or sum type for the algebraic type `RawMiscModuleExportV9`. -export const RawMiscModuleExportV9 = __t.enum('RawMiscModuleExportV9', { +export const RawMiscModuleExportV9 = __t.enum("RawMiscModuleExportV9", { get ColumnDefaultValue() { return RawColumnDefaultValueV9; }, @@ -303,7 +295,7 @@ export const RawMiscModuleExportV9 = __t.enum('RawMiscModuleExportV9', { export type RawMiscModuleExportV9 = __Infer; // The tagged union or sum type for the algebraic type `RawModuleDef`. -export const RawModuleDef = __t.enum('RawModuleDef', { +export const RawModuleDef = __t.enum("RawModuleDef", { get V8BackCompat() { return RawModuleDefV8; }, @@ -316,52 +308,71 @@ export const RawModuleDef = __t.enum('RawModuleDef', { }); export type RawModuleDef = __Infer; -export const RawModuleDefV10 = __t.object('RawModuleDefV10', { - get sections() { +export type RawModuleDefV10 = { + sections: RawModuleDefV10Section[]; +}; + +export const RawModuleDefV10: any = __t.object("RawModuleDefV10", { + get sections(): any { return __t.array(RawModuleDefV10Section); }, }); -export type RawModuleDefV10 = __Infer; // The tagged union or sum type for the algebraic type `RawModuleDefV10Section`. -export const RawModuleDefV10Section = __t.enum('RawModuleDefV10Section', { - get Typespace() { +export type RawModuleDefV10Section = + | { tag: 'Typespace'; value: Typespace } + | { tag: 'Types'; value: RawTypeDefV10[] } + | { tag: 'Tables'; value: RawTableDefV10[] } + | { tag: 'Reducers'; value: RawReducerDefV10[] } + | { tag: 'Procedures'; value: RawProcedureDefV10[] } + | { tag: 'Views'; value: RawViewDefV10[] } + | { tag: 'Schedules'; value: RawScheduleDefV10[] } + | { tag: 'LifeCycleReducers'; value: RawLifeCycleReducerDefV10[] } + | { tag: 'RowLevelSecurity'; value: RawRowLevelSecurityDefV9[] } + | { tag: 'CaseConversionPolicy'; value: CaseConversionPolicy } + | { tag: 'ExplicitNames'; value: ExplicitNames } + | { tag: 'Mounts'; value: RawModuleMountV10[] }; + +export const RawModuleDefV10Section: any = __t.enum("RawModuleDefV10Section", { + get Typespace(): any { return Typespace; }, - get Types() { + get Types(): any { return __t.array(RawTypeDefV10); }, - get Tables() { + get Tables(): any { return __t.array(RawTableDefV10); }, - get Reducers() { + get Reducers(): any { return __t.array(RawReducerDefV10); }, - get Procedures() { + get Procedures(): any { return __t.array(RawProcedureDefV10); }, - get Views() { + get Views(): any { return __t.array(RawViewDefV10); }, - get Schedules() { + get Schedules(): any { return __t.array(RawScheduleDefV10); }, - get LifeCycleReducers() { + get LifeCycleReducers(): any { return __t.array(RawLifeCycleReducerDefV10); }, - get RowLevelSecurity() { + get RowLevelSecurity(): any { return __t.array(RawRowLevelSecurityDefV9); }, - get CaseConversionPolicy() { + get CaseConversionPolicy(): any { return CaseConversionPolicy; }, - get ExplicitNames() { + get ExplicitNames(): any { return ExplicitNames; }, + get Mounts(): any { + return __t.array(RawModuleMountV10); + }, }); -export type RawModuleDefV10Section = __Infer; -export const RawModuleDefV8 = __t.object('RawModuleDefV8', { +export const RawModuleDefV8 = __t.object("RawModuleDefV8", { get typespace() { return Typespace; }, @@ -377,7 +388,7 @@ export const RawModuleDefV8 = __t.object('RawModuleDefV8', { }); export type RawModuleDefV8 = __Infer; -export const RawModuleDefV9 = __t.object('RawModuleDefV9', { +export const RawModuleDefV9 = __t.object("RawModuleDefV9", { get typespace() { return Typespace; }, @@ -399,7 +410,19 @@ export const RawModuleDefV9 = __t.object('RawModuleDefV9', { }); export type RawModuleDefV9 = __Infer; -export const RawProcedureDefV10 = __t.object('RawProcedureDefV10', { +export type RawModuleMountV10 = { + namespace: string; + module: RawModuleDefV10; +}; + +export const RawModuleMountV10: any = __t.object("RawModuleMountV10", { + namespace: __t.string(), + get module(): any { + return RawModuleDefV10; + }, +}); + +export const RawProcedureDefV10 = __t.object("RawProcedureDefV10", { sourceName: __t.string(), get params() { return ProductType; @@ -413,7 +436,7 @@ export const RawProcedureDefV10 = __t.object('RawProcedureDefV10', { }); export type RawProcedureDefV10 = __Infer; -export const RawProcedureDefV9 = __t.object('RawProcedureDefV9', { +export const RawProcedureDefV9 = __t.object("RawProcedureDefV9", { name: __t.string(), get params() { return ProductType; @@ -424,7 +447,7 @@ export const RawProcedureDefV9 = __t.object('RawProcedureDefV9', { }); export type RawProcedureDefV9 = __Infer; -export const RawReducerDefV10 = __t.object('RawReducerDefV10', { +export const RawReducerDefV10 = __t.object("RawReducerDefV10", { sourceName: __t.string(), get params() { return ProductType; @@ -441,7 +464,7 @@ export const RawReducerDefV10 = __t.object('RawReducerDefV10', { }); export type RawReducerDefV10 = __Infer; -export const RawReducerDefV9 = __t.object('RawReducerDefV9', { +export const RawReducerDefV9 = __t.object("RawReducerDefV9", { name: __t.string(), get params() { return ProductType; @@ -452,12 +475,12 @@ export const RawReducerDefV9 = __t.object('RawReducerDefV9', { }); export type RawReducerDefV9 = __Infer; -export const RawRowLevelSecurityDefV9 = __t.object('RawRowLevelSecurityDefV9', { +export const RawRowLevelSecurityDefV9 = __t.object("RawRowLevelSecurityDefV9", { sql: __t.string(), }); export type RawRowLevelSecurityDefV9 = __Infer; -export const RawScheduleDefV10 = __t.object('RawScheduleDefV10', { +export const RawScheduleDefV10 = __t.object("RawScheduleDefV10", { sourceName: __t.option(__t.string()), tableName: __t.string(), scheduleAtCol: __t.u16(), @@ -465,26 +488,26 @@ export const RawScheduleDefV10 = __t.object('RawScheduleDefV10', { }); export type RawScheduleDefV10 = __Infer; -export const RawScheduleDefV9 = __t.object('RawScheduleDefV9', { +export const RawScheduleDefV9 = __t.object("RawScheduleDefV9", { name: __t.option(__t.string()), reducerName: __t.string(), scheduledAtColumn: __t.u16(), }); export type RawScheduleDefV9 = __Infer; -export const RawScopedTypeNameV10 = __t.object('RawScopedTypeNameV10', { +export const RawScopedTypeNameV10 = __t.object("RawScopedTypeNameV10", { scope: __t.array(__t.string()), sourceName: __t.string(), }); export type RawScopedTypeNameV10 = __Infer; -export const RawScopedTypeNameV9 = __t.object('RawScopedTypeNameV9', { +export const RawScopedTypeNameV9 = __t.object("RawScopedTypeNameV9", { scope: __t.array(__t.string()), name: __t.string(), }); export type RawScopedTypeNameV9 = __Infer; -export const RawSequenceDefV10 = __t.object('RawSequenceDefV10', { +export const RawSequenceDefV10 = __t.object("RawSequenceDefV10", { sourceName: __t.option(__t.string()), column: __t.u16(), start: __t.option(__t.i128()), @@ -494,7 +517,7 @@ export const RawSequenceDefV10 = __t.object('RawSequenceDefV10', { }); export type RawSequenceDefV10 = __Infer; -export const RawSequenceDefV8 = __t.object('RawSequenceDefV8', { +export const RawSequenceDefV8 = __t.object("RawSequenceDefV8", { sequenceName: __t.string(), colPos: __t.u16(), increment: __t.i128(), @@ -505,7 +528,7 @@ export const RawSequenceDefV8 = __t.object('RawSequenceDefV8', { }); export type RawSequenceDefV8 = __Infer; -export const RawSequenceDefV9 = __t.object('RawSequenceDefV9', { +export const RawSequenceDefV9 = __t.object("RawSequenceDefV9", { name: __t.option(__t.string()), column: __t.u16(), start: __t.option(__t.i128()), @@ -515,7 +538,7 @@ export const RawSequenceDefV9 = __t.object('RawSequenceDefV9', { }); export type RawSequenceDefV9 = __Infer; -export const RawTableDefV10 = __t.object('RawTableDefV10', { +export const RawTableDefV10 = __t.object("RawTableDefV10", { sourceName: __t.string(), productTypeRef: __t.u32(), primaryKey: __t.array(__t.u16()), @@ -541,7 +564,7 @@ export const RawTableDefV10 = __t.object('RawTableDefV10', { }); export type RawTableDefV10 = __Infer; -export const RawTableDefV8 = __t.object('RawTableDefV8', { +export const RawTableDefV8 = __t.object("RawTableDefV8", { tableName: __t.string(), get columns() { return __t.array(RawColumnDefV8); @@ -561,7 +584,7 @@ export const RawTableDefV8 = __t.object('RawTableDefV8', { }); export type RawTableDefV8 = __Infer; -export const RawTableDefV9 = __t.object('RawTableDefV9', { +export const RawTableDefV9 = __t.object("RawTableDefV9", { name: __t.string(), productTypeRef: __t.u32(), primaryKey: __t.array(__t.u16()), @@ -586,7 +609,7 @@ export const RawTableDefV9 = __t.object('RawTableDefV9', { }); export type RawTableDefV9 = __Infer; -export const RawTypeDefV10 = __t.object('RawTypeDefV10', { +export const RawTypeDefV10 = __t.object("RawTypeDefV10", { get sourceName() { return RawScopedTypeNameV10; }, @@ -595,7 +618,7 @@ export const RawTypeDefV10 = __t.object('RawTypeDefV10', { }); export type RawTypeDefV10 = __Infer; -export const RawTypeDefV9 = __t.object('RawTypeDefV9', { +export const RawTypeDefV9 = __t.object("RawTypeDefV9", { get name() { return RawScopedTypeNameV9; }, @@ -604,17 +627,12 @@ export const RawTypeDefV9 = __t.object('RawTypeDefV9', { }); export type RawTypeDefV9 = __Infer; -export const RawUniqueConstraintDataV9 = __t.object( - 'RawUniqueConstraintDataV9', - { - columns: __t.array(__t.u16()), - } -); -export type RawUniqueConstraintDataV9 = __Infer< - typeof RawUniqueConstraintDataV9 ->; - -export const RawViewDefV10 = __t.object('RawViewDefV10', { +export const RawUniqueConstraintDataV9 = __t.object("RawUniqueConstraintDataV9", { + columns: __t.array(__t.u16()), +}); +export type RawUniqueConstraintDataV9 = __Infer; + +export const RawViewDefV10 = __t.object("RawViewDefV10", { sourceName: __t.string(), index: __t.u32(), isPublic: __t.bool(), @@ -628,7 +646,7 @@ export const RawViewDefV10 = __t.object('RawViewDefV10', { }); export type RawViewDefV10 = __Infer; -export const RawViewDefV9 = __t.object('RawViewDefV9', { +export const RawViewDefV9 = __t.object("RawViewDefV9", { name: __t.string(), index: __t.u32(), isPublic: __t.bool(), @@ -642,7 +660,7 @@ export const RawViewDefV9 = __t.object('RawViewDefV9', { }); export type RawViewDefV9 = __Infer; -export const ReducerDef = __t.object('ReducerDef', { +export const ReducerDef = __t.object("ReducerDef", { name: __t.string(), get args() { return __t.array(ProductTypeElement); @@ -650,14 +668,14 @@ export const ReducerDef = __t.object('ReducerDef', { }); export type ReducerDef = __Infer; -export const SumType = __t.object('SumType', { +export const SumType = __t.object("SumType", { get variants() { return __t.array(SumTypeVariant); }, }); export type SumType = __Infer; -export const SumTypeVariant = __t.object('SumTypeVariant', { +export const SumTypeVariant = __t.object("SumTypeVariant", { name: __t.option(__t.string()), get algebraicType() { return AlgebraicType; @@ -666,13 +684,13 @@ export const SumTypeVariant = __t.object('SumTypeVariant', { export type SumTypeVariant = __Infer; // The tagged union or sum type for the algebraic type `TableAccess`. -export const TableAccess = __t.enum('TableAccess', { +export const TableAccess = __t.enum("TableAccess", { Public: __t.unit(), Private: __t.unit(), }); export type TableAccess = __Infer; -export const TableDesc = __t.object('TableDesc', { +export const TableDesc = __t.object("TableDesc", { get schema() { return RawTableDefV8; }, @@ -681,19 +699,19 @@ export const TableDesc = __t.object('TableDesc', { export type TableDesc = __Infer; // The tagged union or sum type for the algebraic type `TableType`. -export const TableType = __t.enum('TableType', { +export const TableType = __t.enum("TableType", { System: __t.unit(), User: __t.unit(), }); export type TableType = __Infer; -export const TypeAlias = __t.object('TypeAlias', { +export const TypeAlias = __t.object("TypeAlias", { name: __t.string(), ty: __t.u32(), }); export type TypeAlias = __Infer; -export const Typespace = __t.object('Typespace', { +export const Typespace = __t.object("Typespace", { get types() { return __t.array(AlgebraicType); }, @@ -701,8 +719,9 @@ export const Typespace = __t.object('Typespace', { export type Typespace = __Infer; // The tagged union or sum type for the algebraic type `ViewResultHeader`. -export const ViewResultHeader = __t.enum('ViewResultHeader', { +export const ViewResultHeader = __t.enum("ViewResultHeader", { RowData: __t.unit(), RawSql: __t.string(), }); export type ViewResultHeader = __Infer; + diff --git a/crates/bindings-typescript/src/lib/schema.ts b/crates/bindings-typescript/src/lib/schema.ts index be9edc9e113..a0c3dea9241 100644 --- a/crates/bindings-typescript/src/lib/schema.ts +++ b/crates/bindings-typescript/src/lib/schema.ts @@ -7,10 +7,20 @@ import { } from './algebraic_type'; import type { CaseConversionPolicy, + ExplicitNames, + RawLifeCycleReducerDefV10, + RawModuleMountV10, RawModuleDefV10, RawModuleDefV10Section, + RawProcedureDefV10, + RawReducerDefV10, + RawRowLevelSecurityDefV9, + RawScheduleDefV10, RawScopedTypeNameV10, RawTableDefV10, + RawTypeDefV10, + RawViewDefV10, + Typespace, } from './autogen/types'; import type { UntypedIndex } from './indexes'; import type { UntypedTableDef } from './table'; @@ -174,7 +184,18 @@ type CompoundTypeCache = Map< >; export type ModuleDef = { - [S in RawModuleDefV10Section as Uncapitalize]: S['value']; + typespace: Typespace; + types: RawTypeDefV10[]; + tables: RawTableDefV10[]; + reducers: RawReducerDefV10[]; + procedures: RawProcedureDefV10[]; + views: RawViewDefV10[]; + schedules: RawScheduleDefV10[]; + lifeCycleReducers: RawLifeCycleReducerDefV10[]; + rowLevelSecurity: RawRowLevelSecurityDefV9[]; + caseConversionPolicy: CaseConversionPolicy; + explicitNames: ExplicitNames; + mounts: RawModuleMountV10[]; }; type Section = RawModuleDefV10Section; @@ -199,6 +220,7 @@ export class ModuleContext { explicitNames: { entries: [], }, + mounts: [], }; get moduleDef(): ModuleDef { @@ -245,9 +267,19 @@ export class ModuleContext { value: module.caseConversionPolicy, } ); + push( + module.mounts && { + tag: 'Mounts', + value: module.mounts, + } + ); return { sections }; } + addMount(mount: RawModuleMountV10) { + this.#moduleDef.mounts.push(mount); + } + /** * Set the case conversion policy for this module. * Called by the settings mechanism. diff --git a/crates/bindings-typescript/src/server/schema.ts b/crates/bindings-typescript/src/server/schema.ts index b9eb258762b..c9a71a69297 100644 --- a/crates/bindings-typescript/src/server/schema.ts +++ b/crates/bindings-typescript/src/server/schema.ts @@ -1,5 +1,6 @@ import { moduleHooks, type ModuleDefaultExport } from 'spacetime:sys@2.0'; import { CaseConversionPolicy, Lifecycle } from '../lib/autogen/types'; +import type { RawModuleDefV10 } from '../lib/autogen/types'; import { type ParamsAsObject, type ParamsObj, @@ -14,6 +15,7 @@ import { } from '../lib/schema'; import type { UntypedTableSchema } from '../lib/table_schema'; import { ColumnBuilder, TypeBuilder } from '../lib/type_builders'; +import { hasOwn } from '../lib/util'; import { makeProcedureExport, type ProcedureExport, @@ -46,6 +48,8 @@ export class SchemaInner< S extends UntypedSchemaDef = UntypedSchemaDef, > extends ModuleContext { schemaType: S; + exportsRegistered = false; + schedulesResolved = false; existingFunctions = new Set(); reducers: Reducers = []; procedures: Procedures = []; @@ -77,6 +81,10 @@ export class SchemaInner< } resolveSchedules() { + if (this.schedulesResolved) { + return; + } + this.schedulesResolved = true; for (const { reducer, scheduleAtCol, tableName } of this.pendingSchedules) { const functionName = this.functionExports.get(reducer()); if (functionName === undefined) { @@ -138,22 +146,8 @@ export class Schema implements ModuleDefaultExport { } [moduleHooks](exports: object) { - // if (!(hasOwn(exports, 'default') && exports.default instanceof Schema)) { - // throw new TypeError('must export schema as default export'); - // } - const registeredSchema = this.#ctx; - for (const [name, moduleExport] of Object.entries(exports)) { - if (name === 'default') continue; - if (!isModuleExport(moduleExport)) { - throw new TypeError( - 'exporting something that is not a spacetime export' - ); - } - checkExportContext(moduleExport, registeredSchema); - moduleExport[registerExport](registeredSchema, name); - } - registeredSchema.resolveSchedules(); - return makeHooks(registeredSchema); + this.buildRawModuleDefV10(exports); + return makeHooks(this.#ctx); } get schemaType(): S { @@ -168,6 +162,18 @@ export class Schema implements ModuleDefaultExport { return this.#ctx.typespace; } + /** Internal: register exports and materialize the RawModuleDefV10 for upload. */ + buildRawModuleDefV10( + exports: object, + opts?: { ignoreNonModuleExports?: boolean } + ): RawModuleDefV10 { + registerModuleExports(this.#ctx, exports, { + ignoreNonModuleExports: opts?.ignoreNonModuleExports ?? false, + }); + this.#ctx.resolveSchedules(); + return this.#ctx.rawModuleDefV10(); + } + /** * Defines a SpacetimeDB reducer function. * @@ -543,18 +549,89 @@ export interface ModuleSettings { CASE_CONVERSION_POLICY?: CaseConversionPolicy; } -export function schema>( - tables: H, +type MountedModuleNamespace = { + default: Schema; + [key: string]: unknown; +}; + +type SchemaEntry = UntypedTableSchema | MountedModuleNamespace; + +type ExtractTableEntries> = { + [K in keyof H as H[K] extends UntypedTableSchema ? K : never]: Extract< + H[K], + UntypedTableSchema + >; +}; + +function isUntypedTableSchema(x: unknown): x is UntypedTableSchema { + return typeof x === 'object' && x !== null && hasOwn(x, 'tableDef'); +} + +function isMountedModuleNamespace(x: unknown): x is MountedModuleNamespace { + return ( + typeof x === 'object' && + x !== null && + hasOwn(x, 'default') && + x.default instanceof Schema + ); +} + +function registerModuleExports( + schema: SchemaInner, + exports: object, + opts?: { ignoreNonModuleExports?: boolean } +) { + if (schema.exportsRegistered) { + return; + } + schema.exportsRegistered = true; + + for (const [name, moduleExport] of Object.entries(exports)) { + if (name === 'default') continue; + if (!isModuleExport(moduleExport)) { + if (opts?.ignoreNonModuleExports) { + continue; + } + throw new TypeError('exporting something that is not a spacetime export'); + } + checkExportContext(moduleExport, schema); + moduleExport[registerExport](schema, name); + } +} + +export function schema>( + entries: H, moduleSettings?: ModuleSettings -): Schema> { - const ctx = new SchemaInner>(ctx => { +): Schema>> { + const ctx = new SchemaInner>>(ctx => { // Apply module settings. if (moduleSettings?.CASE_CONVERSION_POLICY != null) { ctx.setCaseConversionPolicy(moduleSettings.CASE_CONVERSION_POLICY); } const tableSchemas: Record = {}; - for (const [accName, table] of Object.entries(tables)) { + for (const [accName, entry] of Object.entries(entries)) { + if (entry instanceof Schema) { + throw new TypeError( + `schema entry '${accName}' looks like a default import; use \`import * as ${accName} from '...'\` so the mount can see the library's named reducer exports.` + ); + } + if (isMountedModuleNamespace(entry)) { + ctx.addMount({ + namespace: accName, + module: entry.default.buildRawModuleDefV10(entry, { + ignoreNonModuleExports: true, + }), + }); + continue; + } + if (!isUntypedTableSchema(entry)) { + throw new TypeError( + `schema entry '${accName}' must be a table or a mounted module namespace object` + ); + } + + const table = entry; const tableDef = table.tableDef(ctx, accName); tableSchemas[accName] = tableToSchema(accName, table, tableDef); ctx.moduleDef.tables.push(tableDef); @@ -574,7 +651,7 @@ export function schema>( }); } } - return { tables: tableSchemas } as TablesToSchema; + return { tables: tableSchemas } as TablesToSchema>; }); return new Schema(ctx); diff --git a/crates/bindings-typescript/tests/schema_mounts.test.ts b/crates/bindings-typescript/tests/schema_mounts.test.ts new file mode 100644 index 00000000000..4fe3acb41a3 --- /dev/null +++ b/crates/bindings-typescript/tests/schema_mounts.test.ts @@ -0,0 +1,117 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest'; + +vi.mock( + 'spacetime:sys@2.0', + () => ({ + moduleHooks: Symbol('moduleHooks'), + }), + { virtual: true } +); + +vi.mock('spacetime:sys@2.1', () => ({}), { virtual: true }); + +vi.mock('../src/server/runtime', () => ({ + makeHooks: () => ({}), + callProcedure: () => new Uint8Array(), + callUserFunction: (fn: (...args: any[]) => any, ...args: any[]) => + fn(...args), + ReducerCtxImpl: class {}, + sys: { + row_iter_bsatn_close: () => {}, + }, +})); + +describe('schema mounts', () => { + let schema: typeof import('../src/server/schema').schema; + let table: typeof import('../src/lib/table').table; + let t: typeof import('../src/lib/type_builders').t; + + beforeAll(async () => { + ({ schema } = await import('../src/server/schema')); + ({ table } = await import('../src/lib/table')); + ({ t } = await import('../src/lib/type_builders')); + }); + + it('emits mounted submodule module defs and resolves mounted schedules', () => { + const players = table({ name: 'players' }, { id: t.u32().primaryKey() }); + + const sessionCleanupTick = table( + { + name: 'session_cleanup_tick', + scheduled: (): any => cleanExpiredSessions, + }, + { + scheduledId: t.u64().primaryKey().autoInc(), + scheduledAt: t.scheduleAt(), + } + ); + + const sessions = table( + { name: 'sessions' }, + { + id: t.u64().primaryKey().autoInc(), + } + ); + + const authSchema = schema({ + sessions, + sessionCleanupTick, + }); + + const cleanExpiredSessions = authSchema.reducer(() => {}); + const authLib = { + default: authSchema, + cleanExpiredSessions, + }; + + const consumer = schema({ + players, + myauth: authLib, + }); + + const raw = consumer.buildRawModuleDefV10({}); + const mounts = raw.sections.find( + section => section.tag === 'Mounts' + )?.value; + + expect(mounts).toHaveLength(1); + expect(mounts?.[0]?.namespace).toBe('myauth'); + + const mountedSections = mounts?.[0]?.module.sections ?? []; + const mountedReducers = mountedSections.find( + section => section.tag === 'Reducers' + )?.value; + const mountedSchedules = mountedSections.find( + section => section.tag === 'Schedules' + )?.value; + + expect(mountedReducers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ sourceName: 'cleanExpiredSessions' }), + ]) + ); + expect(mountedSchedules).toEqual([ + expect.objectContaining({ + tableName: 'sessionCleanupTick', + functionName: 'cleanExpiredSessions', + }), + ]); + }); + + it('rejects default-import style mounts with a clear error', () => { + const sessions = table( + { name: 'sessions' }, + { + id: t.u64().primaryKey().autoInc(), + } + ); + + const authSchema = schema({ sessions }); + + expect(() => + schema({ + myauth: authSchema as any, + }) + ).toThrow(/looks like a default import/); + }); +}); diff --git a/crates/codegen/src/cpp.rs b/crates/codegen/src/cpp.rs index 3f20b4d271a..c88a947c25f 100644 --- a/crates/codegen/src/cpp.rs +++ b/crates/codegen/src/cpp.rs @@ -136,6 +136,110 @@ impl<'opts> Cpp<'opts> { } // Generate minimal product type (struct with fields only) + /// Rename C++ keyword field names by appending `_`. + /// BSATN serialization is positional, so the rename is wire-compatible. + fn cpp_safe_field_name(name: &str) -> String { + const CPP_KEYWORDS: &[&str] = &[ + "alignas", + "alignof", + "and", + "and_eq", + "asm", + "auto", + "bitand", + "bitor", + "bool", + "break", + "case", + "catch", + "char", + "char8_t", + "char16_t", + "char32_t", + "class", + "compl", + "concept", + "const", + "consteval", + "constexpr", + "constinit", + "const_cast", + "continue", + "co_await", + "co_return", + "co_yield", + "decltype", + "default", + "delete", + "do", + "double", + "dynamic_cast", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "inline", + "int", + "long", + "mutable", + "namespace", + "new", + "noexcept", + "not", + "not_eq", + "nullptr", + "operator", + "or", + "or_eq", + "private", + "protected", + "public", + "register", + "reinterpret_cast", + "requires", + "return", + "short", + "signed", + "sizeof", + "static", + "static_assert", + "static_cast", + "struct", + "switch", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typedef", + "typeid", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "wchar_t", + "while", + "xor", + "xor_eq", + ]; + if CPP_KEYWORDS.contains(&name) { + format!("{name}_") + } else { + name.to_string() + } + } + fn write_product_type(&self, output: &mut String, module: &ModuleDef, type_name: &str, product: &ProductTypeDef) { // Use INTERNAL macro for Internal namespace let macro_name = if self.namespace == "SpacetimeDB::Internal" { @@ -147,9 +251,10 @@ impl<'opts> Cpp<'opts> { // Write fields only for (field_name, field_type) in &product.elements { + let safe_name = Self::cpp_safe_field_name(field_name); write!(output, " ").unwrap(); self.write_algebraic_type(output, module, field_type).unwrap(); - writeln!(output, " {};", field_name).unwrap(); + writeln!(output, " {};", safe_name).unwrap(); } writeln!(output).unwrap(); @@ -161,10 +266,11 @@ impl<'opts> Cpp<'opts> { ) .unwrap(); for (field_name, _) in &product.elements { + let safe_name = Self::cpp_safe_field_name(field_name); writeln!( output, " ::SpacetimeDB::bsatn::serialize(writer, {});", - field_name + safe_name ) .unwrap(); } @@ -174,10 +280,11 @@ impl<'opts> Cpp<'opts> { if !product.elements.is_empty() { write!(output, " SPACETIMEDB_PRODUCT_TYPE_EQUALITY(").unwrap(); for (i, (field_name, _)) in product.elements.iter().enumerate() { + let safe_name = Self::cpp_safe_field_name(field_name); if i > 0 { write!(output, ", ").unwrap(); } - write!(output, "{}", field_name).unwrap(); + write!(output, "{}", safe_name).unwrap(); } writeln!(output, ")").unwrap(); } @@ -185,6 +292,53 @@ impl<'opts> Cpp<'opts> { writeln!(output, "}};").unwrap(); } + fn generate_raw_module_mount_v10_special(&self) -> String { + // RawModuleMountV10 is special for two reasons: + // 1. Its `namespace` field is a C++ keyword, renamed to `namespace_`. + // 2. It contains `RawModuleDefV10` which creates a circular include chain: + // RawModuleMountV10 → RawModuleDefV10 → RawModuleDefV10Section → RawModuleMountV10 + // We break this with a forward declaration and shared_ptr (which only needs a declaration). + r#"// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +// This was generated using spacetimedb codegen. + +#pragma once + +#include +#include +#include +#include +#include +#include "../autogen_base.h" +#include "spacetimedb/bsatn/bsatn.h" + +// Forward declaration breaks the circular include chain: +// RawModuleMountV10 -> RawModuleDefV10 -> RawModuleDefV10Section -> RawModuleMountV10 +namespace SpacetimeDB::Internal { struct RawModuleDefV10; } + +namespace SpacetimeDB::Internal { + +SPACETIMEDB_INTERNAL_PRODUCT_TYPE(RawModuleMountV10) { + std::string namespace_; // renamed: 'namespace' is a C++ keyword + std::shared_ptr module; // shared_ptr breaks infinite-size recursion + + void bsatn_serialize(::SpacetimeDB::bsatn::Writer& writer) const { + ::SpacetimeDB::bsatn::serialize(writer, namespace_); + if (module) ::SpacetimeDB::bsatn::serialize(writer, *module); + } + bool operator==(const RawModuleMountV10& o) const noexcept { + if (namespace_ != o.namespace_) return false; + if (module && o.module) return *module == *o.module; + return !module && !o.module; + } + bool operator!=(const RawModuleMountV10& o) const noexcept { return !(*this == o); } +}; +} // namespace SpacetimeDB::Internal +"# + .to_string() + } + // Generate minimal sum type (TaggedEnum only) fn write_sum_type(&self, output: &mut String, module: &ModuleDef, type_name: &str, sum: &SumTypeDef) { // Special case: Generate proper tagged enum for RawIndexAlgorithm with data variants @@ -483,6 +637,17 @@ impl Lang for Cpp<'_> { }]; } + // Special handling for RawModuleMountV10: + // (1) its `namespace` field is a C++ keyword; (2) its `module` field creates a + // circular include chain through RawModuleDefV10 → RawModuleDefV10Section. + // We break both with a forward declaration and shared_ptr. + if name.to_string() == "RawModuleMountV10" { + return vec![OutputFile { + filename: format!("{name}.g.h"), + code: self.generate_raw_module_mount_v10_special(), + }]; + } + self.write_standard_includes(&mut output); // Add includes for dependencies diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 28bc1fb4c91..41a629df76c 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -475,6 +475,81 @@ fn generate_procedures_file(module: &ModuleDef, options: &CodegenOptions) -> Out } } +/// Collect all `AlgebraicTypeRef`s directly referenced by a type def. +fn direct_refs_of_def(def: &AlgebraicTypeDef) -> Vec { + fn refs_of_use(ty: &AlgebraicTypeUse, out: &mut Vec) { + match ty { + AlgebraicTypeUse::Ref(r) => out.push(*r), + AlgebraicTypeUse::Array(e) | AlgebraicTypeUse::Option(e) => refs_of_use(e, out), + AlgebraicTypeUse::Result { ok_ty, err_ty } => { + refs_of_use(ok_ty, out); + refs_of_use(err_ty, out); + } + _ => {} + } + } + let mut refs = Vec::new(); + match def { + AlgebraicTypeDef::Product(p) => { + for (_, ty) in &p.elements { + refs_of_use(ty, &mut refs); + } + } + AlgebraicTypeDef::Sum(s) => { + for (_, ty) in &s.variants { + refs_of_use(ty, &mut refs); + } + } + AlgebraicTypeDef::PlainEnum(_) => {} + } + refs +} + +/// BFS to find all type refs reachable from `start` in the type-dependency graph. +fn reachable_from( + typespace: &spacetimedb_schema::type_for_generate::TypespaceForGenerate, + start: AlgebraicTypeRef, +) -> BTreeSet { + let mut visited = BTreeSet::new(); + let mut stack = vec![start]; + while let Some(r) = stack.pop() { + if !visited.insert(r) { + continue; + } + if let Some(def) = typespace.get(r) { + for neighbor in direct_refs_of_def(def) { + stack.push(neighbor); + } + } + } + visited +} + +/// Compute the SCC containing `AlgebraicType` (the built-in SATS type). +/// +/// Types in this SCC — `AlgebraicType`, `SumType`, `ProductType`, `SumTypeVariant`, +/// `ProductTypeElement` — use the existing getter pattern instead of `__t.lazy()`. +/// The `AlgebraicType` const has an explicit `__TypeBuilder<__AlgebraicTypeType, …>` +/// annotation that breaks the TypeScript type-inference cycle for the whole SCC. +/// Making these types lazy would annotate them as `any`, which invalidates that annotation. +fn algebraic_type_scc(module: &ModuleDef) -> BTreeSet { + let Some(at_ref) = iter_types(module) + .find(|ty| type_ref_name(module, ty.ty) == "AlgebraicType") + .map(|ty| ty.ty) + else { + return BTreeSet::new(); + }; + + let typespace = module.typespace_for_generate(); + let from_at = reachable_from(typespace, at_ref); + // SCC = types reachable from AlgebraicType that can also reach back to AlgebraicType. + from_at + .iter() + .filter(|&&r| reachable_from(typespace, r).contains(&at_ref)) + .copied() + .collect() +} + fn generate_types_file(module: &ModuleDef) -> OutputFile { let mut output = CodeIndenter::new(String::new(), INDENT); let out = &mut output; @@ -487,15 +562,24 @@ fn generate_types_file(module: &ModuleDef) -> OutputFile { .map(|reducer| reducer.accessor_name.deref().to_case(Case::Pascal)) .collect::>(); + // Types in the AlgebraicType SCC must not use __t.lazy(); the existing getter + // pattern handles their mutual recursion, and AlgebraicType's explicit type + // annotation breaks the TypeScript inference cycle for the whole SCC. + let algebraic_scc = algebraic_type_scc(module); + for ty in iter_types(module) { let type_name = collect_case(Case::Pascal, ty.accessor_name.name_segments()); if reducer_type_names.contains(&type_name) { continue; } - match &module.typespace_for_generate()[ty.ty] { - AlgebraicTypeDef::Product(product) => define_body_for_product(module, out, &type_name, &product.elements), - AlgebraicTypeDef::Sum(sum) => define_body_for_sum(module, out, &type_name, &sum.variants), + let type_def = &module.typespace_for_generate()[ty.ty]; + let is_recursive = type_def.is_recursive() && !algebraic_scc.contains(&ty.ty); + match type_def { + AlgebraicTypeDef::Product(product) => { + define_body_for_product(module, out, &type_name, &product.elements, is_recursive) + } + AlgebraicTypeDef::Sum(sum) => define_body_for_sum(module, out, &type_name, &sum.variants, is_recursive), AlgebraicTypeDef::PlainEnum(plain_enum) => { let variants = plain_enum .variants @@ -503,7 +587,7 @@ fn generate_types_file(module: &ModuleDef) -> OutputFile { .cloned() .map(|var| (var, AlgebraicTypeUse::Unit)) .collect::>(); - define_body_for_sum(module, out, &type_name, &variants) + define_body_for_sum(module, out, &type_name, &variants, false) } } } @@ -514,6 +598,105 @@ fn generate_types_file(module: &ModuleDef) -> OutputFile { } } +/// Converts an `AlgebraicTypeUse` to a TypeScript type expression for use in explicit type aliases. +/// Used when generating `export type Foo = { ... }` for recursive types. +fn ts_type_use(module: &ModuleDef, ty: &AlgebraicTypeUse) -> String { + match ty { + AlgebraicTypeUse::String => "string".to_string(), + AlgebraicTypeUse::Primitive(PrimitiveType::Bool) => "boolean".to_string(), + AlgebraicTypeUse::Primitive( + PrimitiveType::I8 + | PrimitiveType::U8 + | PrimitiveType::I16 + | PrimitiveType::U16 + | PrimitiveType::I32 + | PrimitiveType::U32 + | PrimitiveType::F32 + | PrimitiveType::F64, + ) => "number".to_string(), + AlgebraicTypeUse::Primitive(_) => "bigint".to_string(), + AlgebraicTypeUse::Array(elem) if matches!(&**elem, AlgebraicTypeUse::Primitive(PrimitiveType::U8)) => { + "Uint8Array".to_string() + } + AlgebraicTypeUse::Array(elem) => format!("{}[]", ts_type_use(module, elem)), + AlgebraicTypeUse::Option(inner) => format!("{} | undefined", ts_type_use(module, inner)), + AlgebraicTypeUse::Ref(r) => type_ref_name(module, *r), + AlgebraicTypeUse::Unit => "Record".to_string(), + AlgebraicTypeUse::Never => "never".to_string(), + AlgebraicTypeUse::Identity => "__Identity".to_string(), + AlgebraicTypeUse::ConnectionId => "__ConnectionId".to_string(), + AlgebraicTypeUse::Timestamp => "__Timestamp".to_string(), + AlgebraicTypeUse::TimeDuration => "__TimeDuration".to_string(), + AlgebraicTypeUse::ScheduleAt => "__ScheduleAt".to_string(), + AlgebraicTypeUse::Uuid => "__Uuid".to_string(), + AlgebraicTypeUse::Result { ok_ty, err_ty } => { + format!( + "{{ ok: {} }} | {{ err: {} }}", + ts_type_use(module, ok_ty), + ts_type_use(module, err_ty) + ) + } + } +} + +/// Emits an explicit TypeScript type alias for a product type, used for recursive types. +/// e.g. `export type Foo = { bar: Bar; };` +fn write_ts_type_alias_for_product( + module: &ModuleDef, + out: &mut Indenter, + name: &str, + elements: &[(Identifier, AlgebraicTypeUse)], +) { + writeln!(out, "export type {name} = {{"); + out.indent(1); + for (ident, ty) in elements { + let field_name = ident.deref().to_case(Case::Camel); + writeln!(out, "{field_name}: {};", ts_type_use(module, ty)); + } + out.dedent(1); + writeln!(out, "}};"); + out.newline(); +} + +/// Emits an explicit TypeScript type alias for a sum type, used for recursive types. +/// e.g. `export type Foo = | { tag: 'Bar'; value: Bar } | { tag: 'Baz'; value: Baz };` +fn write_ts_type_alias_for_sum( + module: &ModuleDef, + out: &mut Indenter, + name: &str, + variants: &[(Identifier, AlgebraicTypeUse)], +) { + writeln!(out, "export type {name} ="); + out.indent(1); + for (i, (ident, ty)) in variants.iter().enumerate() { + let variant_name = ident.deref().to_case(Case::Pascal); + let is_last = i == variants.len() - 1; + let semicolon = if is_last { ";" } else { "" }; + writeln!( + out, + "| {{ tag: '{variant_name}'; value: {} }}{semicolon}", + ts_type_use(module, ty) + ); + } + out.dedent(1); + out.newline(); +} + +/// Emits a getter field with an explicit `: any` return-type annotation. +/// +/// Required for getters inside `const X: any = __t.object/enum(…)` declarations of +/// recursive types: without the annotation TypeScript emits TS7023 ("implicitly has +/// return type 'any' because it is referenced in its own return expressions"). +fn write_any_getter_field(module: &ModuleDef, out: &mut Indenter, name: &str, ty: &AlgebraicTypeUse) { + writeln!(out, "get {name}(): any {{"); + out.indent(1); + write!(out, "return "); + write_type_builder(module, out, ty).unwrap(); + writeln!(out, ";"); + out.dedent(1); + writeln!(out, "}},"); +} + fn print_index_imports(out: &mut Indenter) { // All library imports are prefixed with `__` to avoid // clashing with the names of user generated types. @@ -624,16 +807,41 @@ fn define_body_for_product( out: &mut Indenter, name: &str, elements: &[(Identifier, AlgebraicTypeUse)], + is_recursive: bool, ) { - write!(out, "export const {name} = __t.object(\"{name}\", {{"); - if elements.is_empty() { + if is_recursive { + // Emit an explicit TS type alias to break the circular type-inference chain, + // then annotate the const as `any` (required — there is no external anchor like + // AlgebraicType's __TypeBuilder annotation to terminate inference). Getters on + // ref-containing fields need `: any` return types to suppress TS7023. + write_ts_type_alias_for_product(module, out, name, elements); + writeln!(out, "export const {name}: any = __t.object(\"{name}\", {{"); + if !elements.is_empty() { + out.with_indent(|out| { + for (ident, ty) in elements { + let field_name = ident.deref().to_case(Case::Camel); + if type_contains_ref(ty) { + write_any_getter_field(module, out, &field_name, ty); + } else { + write!(out, "{field_name}: "); + write_type_builder(module, out, ty).unwrap(); + writeln!(out, ","); + } + } + }); + } writeln!(out, "}});"); } else { - writeln!(out); - out.with_indent(|out| write_object_type_builder_fields(module, out, elements, None, true, false).unwrap()); - writeln!(out, "}});"); + write!(out, "export const {name} = __t.object(\"{name}\", {{"); + if elements.is_empty() { + writeln!(out, "}});"); + } else { + writeln!(out); + out.with_indent(|out| write_object_type_builder_fields(module, out, elements, None, true, false).unwrap()); + writeln!(out, "}});"); + } + writeln!(out, "export type {name} = __Infer;"); } - writeln!(out, "export type {name} = __Infer;"); out.newline(); } @@ -858,13 +1066,8 @@ fn define_body_for_sum( out: &mut Indenter, name: &str, variants: &[(Identifier, AlgebraicTypeUse)], + is_recursive: bool, ) { - writeln!(out, "// The tagged union or sum type for the algebraic type `{name}`."); - write!(out, "export const {name}"); - if name == "AlgebraicType" { - write!(out, ": __TypeBuilder<__AlgebraicTypeType, __AlgebraicTypeType>"); - } - writeln!(out, " = __t.enum(\"{name}\", {{"); // Convert variant names to PascalCase let pascal_variants: Vec<(Identifier, AlgebraicTypeUse)> = variants .iter() @@ -873,9 +1076,35 @@ fn define_body_for_sum( (Identifier::for_test(pascal), ty.clone()) }) .collect(); - out.with_indent(|out| write_object_type_builder_fields(module, out, &pascal_variants, None, false, false).unwrap()); - writeln!(out, "}});"); - writeln!(out, "export type {name} = __Infer;"); + + writeln!(out, "// The tagged union or sum type for the algebraic type `{name}`."); + if is_recursive { + write_ts_type_alias_for_sum(module, out, name, variants); + writeln!(out, "export const {name}: any = __t.enum(\"{name}\", {{"); + out.with_indent(|out| { + for (ident, ty) in &pascal_variants { + if type_contains_ref(ty) { + write_any_getter_field(module, out, ident.deref(), ty); + } else { + write!(out, "{}: ", ident.deref()); + write_type_builder(module, out, ty).unwrap(); + writeln!(out, ","); + } + } + }); + writeln!(out, "}});"); + } else { + write!(out, "export const {name}"); + if name == "AlgebraicType" { + write!(out, ": __TypeBuilder<__AlgebraicTypeType, __AlgebraicTypeType>"); + } + writeln!(out, " = __t.enum(\"{name}\", {{"); + out.with_indent(|out| { + write_object_type_builder_fields(module, out, &pascal_variants, None, false, false).unwrap() + }); + writeln!(out, "}});"); + writeln!(out, "export type {name} = __Infer;"); + } out.newline(); } diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index a801ea286be..038c53d69d6 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -89,6 +89,17 @@ pub enum RawModuleDefV10Section { /// Names provided explicitly by the user that do not follow from the case conversion policy. ExplicitNames(ExplicitNames), + + /// Mounted submodules, keyed by the namespace they are mounted under. + Mounts(Vec), +} + +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawModuleMountV10 { + pub namespace: String, + pub module: RawModuleDefV10, } #[derive(Debug, Clone, Copy, Default, SpacetimeType)] @@ -510,6 +521,14 @@ pub struct RawViewDefV10 { } impl RawModuleDefV10 { + /// Get the mounted submodules for this module definition. + pub fn mounts(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Mounts(mounts) => Some(mounts), + _ => None, + }) + } + /// Get the types section, if present. pub fn types(&self) -> Option<&Vec> { self.sections.iter().find_map(|s| match s { diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 89c201e3f85..9892500ad0c 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -33,8 +33,8 @@ use spacetimedb_data_structures::map::{Equivalent, HashMap}; use spacetimedb_lib::db::raw_def; use spacetimedb_lib::db::raw_def::v10::{ ExplicitNames, RawConstraintDefV10, RawIndexDefV10, RawLifeCycleReducerDefV10, RawModuleDefV10, - RawModuleDefV10Section, RawProcedureDefV10, RawReducerDefV10, RawRowLevelSecurityDefV10, RawScheduleDefV10, - RawScopedTypeNameV10, RawSequenceDefV10, RawTableDefV10, RawTypeDefV10, RawViewDefV10, + RawModuleDefV10Section, RawModuleMountV10, RawProcedureDefV10, RawReducerDefV10, RawRowLevelSecurityDefV10, + RawScheduleDefV10, RawScopedTypeNameV10, RawSequenceDefV10, RawTableDefV10, RawTypeDefV10, RawViewDefV10, }; use spacetimedb_lib::db::raw_def::v9::{ Lifecycle, RawColumnDefaultValueV9, RawConstraintDataV9, RawConstraintDefV9, RawIndexAlgorithm, RawIndexDefV9, @@ -151,6 +151,9 @@ pub struct ModuleDef { /// was authored under. #[allow(unused)] raw_module_def_version: RawModuleDefVersion, + + /// Mounted submodules, keyed by the namespace they are mounted under. + mounts: IndexMap, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -167,6 +170,11 @@ impl ModuleDef { self.raw_module_def_version } + /// The mounted submodules of the module definition. + pub fn mounts(&self) -> &IndexMap { + &self.mounts + } + /// The tables of the module definition. pub fn tables(&self) -> impl Iterator { self.tables.values() @@ -437,6 +445,7 @@ impl From for RawModuleDefV9 { row_level_security_raw, procedures, raw_module_def_version: _, + mounts: _, } = val; // Extract column defaults from tables before consuming tables @@ -493,6 +502,7 @@ impl From for RawModuleDefV10 { row_level_security_raw, procedures, raw_module_def_version: _, + mounts, } = val; let mut sections = Vec::new(); @@ -605,6 +615,17 @@ impl From for RawModuleDefV10 { // Always emit ExplicitNames so canonical names survive the round-trip. sections.push(RawModuleDefV10Section::ExplicitNames(explicit_names)); + let mounts: Vec<_> = mounts + .into_iter() + .map(|(namespace, module)| RawModuleMountV10 { + namespace, + module: module.into(), + }) + .collect(); + if !mounts.is_empty() { + sections.push(RawModuleDefV10Section::Mounts(mounts)); + } + RawModuleDefV10 { sections } } } diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index e840770a8c2..a949b93d224 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -81,6 +81,12 @@ pub fn validate(def: RawModuleDefV10) -> Result { .cloned() .map(ExplicitNamesLookup::new) .unwrap_or_default(); + let mounts = def + .mounts() + .into_iter() + .flat_map(|mounts| mounts.iter().cloned()) + .map(validate_mount) + .collect_all_errors::>(); // Original `typespace` needs to be preserved to be assign `accesor_name`s to columns. let typespace_with_accessor_names = typespace.clone(); @@ -263,8 +269,12 @@ pub fn validate(def: RawModuleDefV10) -> Result { .map(|rls| (rls.sql.clone(), rls.to_owned())) .collect(); - let (tables, types, reducers, procedures, views) = - (tables_types_reducers_procedures_views).map_err(|errors| errors.sort_deduplicate())?; + let (tables, types, reducers, procedures, views, mounts) = (tables_types_reducers_procedures_views, mounts) + .combine_errors() + .and_then(|((tables, types, reducers, procedures, views), mounts)| { + validate_mount_names_are_unique(mounts).map(|mounts| (tables, types, reducers, procedures, views, mounts)) + }) + .map_err(|errors: ValidationErrors| errors.sort_deduplicate())?; let typespace_for_generate = typespace_for_generate.finish(); @@ -281,9 +291,32 @@ pub fn validate(def: RawModuleDefV10) -> Result { lifecycle_reducers, procedures, raw_module_def_version: RawModuleDefVersion::V10, + mounts, }) } +fn validate_mount(mount: RawModuleMountV10) -> Result<(String, ModuleDef)> { + Identifier::new(mount.namespace.clone().into()) + .map_err(|error| ValidationErrors::from(ValidationError::IdentifierError { error }))?; + + Ok((mount.namespace, validate(mount.module)?)) +} + +fn validate_mount_names_are_unique(mounts: Vec<(String, ModuleDef)>) -> Result> { + let mut errors = vec![]; + let mut map = IndexMap::with_capacity(mounts.len()); + + for (namespace, def) in mounts { + if map.contains_key(&namespace) { + errors.push(ValidationError::DuplicateName { name: namespace.into() }); + } else { + map.insert(namespace, def); + } + } + + ValidationErrors::add_extra_errors(Ok(map), errors) +} + /// Change the visibility of scheduled functions and lifecycle reducers to Internal. /// fn change_scheduled_functions_and_lifetimes_visibility( @@ -882,11 +915,14 @@ mod tests { use itertools::Itertools; use spacetimedb_data_structures::expect_error_matching; - use spacetimedb_lib::db::raw_def::v10::{CaseConversionPolicy, RawModuleDefV10Builder}; + use spacetimedb_lib::db::raw_def::v10::{ + CaseConversionPolicy, RawModuleDefV10, RawModuleDefV10Builder, RawModuleDefV10Section, RawModuleMountV10, + }; use spacetimedb_lib::db::raw_def::v9::{btree, direct, hash}; use spacetimedb_lib::db::raw_def::*; use spacetimedb_lib::ScheduleAt; use spacetimedb_primitives::{ColId, ColList, ColSet}; + use spacetimedb_sats::raw_identifier::RawIdentifier; use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, AlgebraicValue, ProductType, SumValue}; use v9::{Lifecycle, TableAccess, TableType}; @@ -1261,6 +1297,66 @@ mod tests { }); } + #[test] + fn validates_mounted_submodules_recursively() { + let mut mounted_builder = RawModuleDefV10Builder::new(); + mounted_builder + .build_table_with_new_type("Sessions", ProductType::from([("id", AlgebraicType::U64)]), true) + .finish(); + + let raw = RawModuleDefV10 { + sections: vec![RawModuleDefV10Section::Mounts(vec![RawModuleMountV10 { + namespace: "authlib".to_string(), + module: mounted_builder.finish(), + }])], + }; + + let def: ModuleDef = raw.try_into().expect("mounted module should validate"); + let mounts = def.mounts(); + + assert_eq!(mounts.len(), 1); + let mounted = mounts.get("authlib").expect("authlib mount should exist"); + assert!(mounted.table(&expect_identifier("sessions")).is_some()); + } + + #[test] + fn invalid_mount_namespace() { + let raw = RawModuleDefV10 { + sections: vec![RawModuleDefV10Section::Mounts(vec![RawModuleMountV10 { + namespace: "".to_string(), + module: RawModuleDefV10::default(), + }])], + }; + + let result: Result = raw.try_into(); + + expect_error_matching!(result, ValidationError::IdentifierError { error } => { + error == &IdentifierError::Empty {} + }); + } + + #[test] + fn duplicate_mount_namespace() { + let raw = RawModuleDefV10 { + sections: vec![RawModuleDefV10Section::Mounts(vec![ + RawModuleMountV10 { + namespace: "authlib".to_string(), + module: RawModuleDefV10::default(), + }, + RawModuleMountV10 { + namespace: "authlib".to_string(), + module: RawModuleDefV10::default(), + }, + ])], + }; + + let result: Result = raw.try_into(); + + expect_error_matching!(result, ValidationError::DuplicateName { name } => { + name == &RawIdentifier::from("authlib") + }); + } + #[test] fn invalid_unique_constraint_column_ref() { let mut builder = RawModuleDefV10Builder::new(); diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index ec1c2f35919..25b10b8d77d 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -166,6 +166,7 @@ pub fn validate(def: RawModuleDefV9) -> Result { lifecycle_reducers, procedures, raw_module_def_version: RawModuleDefVersion::V9OrEarlier, + mounts: IndexMap::new(), }) }