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..4804b1d01f5 100644 --- a/crates/bindings-typescript/src/lib/autogen/types.ts +++ b/crates/bindings-typescript/src/lib/autogen/types.ts @@ -316,15 +316,50 @@ export const RawModuleDef = __t.enum('RawModuleDef', { }); export type RawModuleDef = __Infer; -export const RawModuleDefV10 = __t.object('RawModuleDefV10', { - get sections() { - return __t.array(RawModuleDefV10Section); - }, -}); -export type RawModuleDefV10 = __Infer; +export type RawModuleDefV10 = { + sections: RawModuleDefV10Section[]; +}; + +export const RawModuleDefV10: any = __t.lazy(() => + __t.object('RawModuleDefV10', { + get sections(): any { + return __t.array(RawModuleDefV10Section); + }, + }) +); + +export type RawModuleDefV10Mount = { + namespace: string; + module: RawModuleDefV10; +}; + +export const RawModuleDefV10Mount: any = __t.lazy(() => + __t.object('RawModuleDefV10Mount', { + get namespace(): any { + return __t.string(); + }, + get module(): any { + return RawModuleDefV10; + }, + }) +); + +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: RawModuleDefV10Mount[] }; // The tagged union or sum type for the algebraic type `RawModuleDefV10Section`. -export const RawModuleDefV10Section = __t.enum('RawModuleDefV10Section', { +export const RawModuleDefV10Section: any = __t.enum('RawModuleDefV10Section', { get Typespace() { return Typespace; }, @@ -358,8 +393,10 @@ export const RawModuleDefV10Section = __t.enum('RawModuleDefV10Section', { get ExplicitNames() { return ExplicitNames; }, + get Mounts(): any { + return __t.array(RawModuleDefV10Mount); + }, }); -export type RawModuleDefV10Section = __Infer; export const RawModuleDefV8 = __t.object('RawModuleDefV8', { get typespace() { diff --git a/crates/bindings-typescript/src/lib/schema.ts b/crates/bindings-typescript/src/lib/schema.ts index be9edc9e113..075ea99d132 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, + RawModuleDefV10Mount, 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: RawModuleDefV10Mount[]; }; 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: RawModuleDefV10Mount) { + 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/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..d518f2ac5ef 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: Vec<(String, ModuleDef)>, } #[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) -> &[(String, ModuleDef)] { + &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 7ebbaae06d4..c0ff11df333 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() + .map(|((tables, types, reducers, procedures, views), 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,17 @@ 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)?)) +} + /// Change the visibility of scheduled functions and lifecycle reducers to Internal. /// fn change_scheduled_functions_and_lifetimes_visibility( @@ -877,7 +895,9 @@ 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; @@ -1256,6 +1276,44 @@ 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); + assert_eq!(mounts[0].0, "authlib"); + assert!(mounts[0].1.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 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 d040435afe5..5daf738a4c0 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: Vec::new(), }) }