diff --git a/docs/content/docs/plugins/cms.mdx b/docs/content/docs/plugins/cms.mdx
index c200a2d7..25a99682 100644
--- a/docs/content/docs/plugins/cms.mdx
+++ b/docs/content/docs/plugins/cms.mdx
@@ -1315,10 +1315,10 @@ export async function generateStaticParams() {
### Server-side mutation — `createContentItem`
-In addition to read-only getters, the CMS plugin exposes a **mutation function** for creating content items directly from server-side code.
+In addition to read-only getters, the CMS plugin exposes a **mutation function** for creating content items directly from server-side code. This is the recommended path for seeds, imports, and scheduled jobs.
-**`createContentItem` bypasses authorization hooks and Zod schema validation.** Hooks such as `onBeforeCreate` and `onAfterCreate` are **not** called, and the data payload is stored as-is without running the content type's schema validation. The caller is responsible for providing valid, relation-free data and for any access-control checks. For relation fields or schema validation, use the HTTP endpoint instead.
+**`createContentItem` bypasses authorization hooks and Zod schema validation.** Hooks such as `onBeforeCreate` and `onAfterCreate` are **not** called, and the data payload is stored as-is without running the content type's schema validation. The caller is responsible for providing valid data and for any access-control checks. Inline `_new` relation creation is not supported — pre-create related items and pass their IDs. For schema validation or inline `_new` creation, use the HTTP endpoint instead.
**Via `myStack.api.cms`:**
@@ -1348,6 +1348,52 @@ await createCMSContentItem(myStack.adapter, "client-profile", {
})
```
+#### `syncRelations` option
+
+By default, `createContentItem` only writes the item's JSON payload — it does **not** populate the `contentRelation` junction table. This is a no-op for content types without relations, but for content types with `belongsTo` / `hasMany` / `manyToMany` fields it means:
+
+- The admin UI's "Related Items" / inverse-relations panel will not discover the item.
+- `useContentByRelation`, `getContentByRelation`, and `*/populated` endpoints will not return it.
+- Only the JSON `{ id: "..." }` reference on the item itself is persisted.
+
+Pass `{ syncRelations: true }` to also persist relation fields into the junction table — the same behavior the HTTP `POST /content/:typeSlug` route provides, minus inline `_new` creation.
+
+```ts
+await myStack.api.cms.createContentItem(
+ "study-reference",
+ {
+ slug: "bpc157-ref-gwyer-2019",
+ data: {
+ compoundId: { id: bpc157.id }, // belongsTo
+ categoryIds: [{ id: catPeptides.id }], // manyToMany
+ author: "Gwyer, D. et al.",
+ year: 2019,
+ title: "BPC-157 promotes angiogenesis…",
+ quote: "…",
+ relevance: "Mechanism of Action",
+ },
+ },
+ { syncRelations: true },
+)
+```
+
+
+Enable `syncRelations: true` whenever the content type has relation fields — especially in seed scripts. Without it, seeded items appear correctly on their own detail page but are invisible to inverse-relation queries, so the admin "Related Items" panel and any `by-relation` filter will silently show zero results.
+
+
+The same option is available on the direct import:
+
+```ts
+import { createCMSContentItem } from "@btst/stack/plugins/cms/api"
+
+await createCMSContentItem(
+ myStack.adapter,
+ "study-reference",
+ { slug: "…", data: { compoundId: { id: bpc157.id }, /* … */ } },
+ { syncRelations: true },
+)
+```
+
Throws if:
- The content type slug is not found (run `ensureSynced` first if calling outside a plugin request)
- A content item with the same slug already exists in that content type
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 6e60fbab..3adb2bba 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@btst/codegen",
- "version": "0.1.2",
+ "version": "0.1.3",
"description": "BTST project scaffolding and CLI passthrough commands.",
"repository": {
"type": "git",
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 3ca036a3..398ce7fc 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -31,6 +31,7 @@ import { installInitDependencies } from "../utils/package-installer";
import {
adapterNeedsGenerate,
getGenerateHintForAdapter,
+ getOutputForAdapter,
runCliPassthrough,
} from "../utils/passthrough";
import { buildScaffoldPlan } from "../utils/scaffold-plan";
@@ -321,8 +322,13 @@ export function createInitCommand() {
const orm = ADAPTERS.find(
(item) => item.key === adapter,
)?.ormForGenerate;
+ const outputPath = getOutputForAdapter(adapter);
const args = orm
- ? [`--orm=${orm}`, `--config=${stackPath}`]
+ ? [
+ `--orm=${orm}`,
+ `--config=${stackPath}`,
+ ...(outputPath ? [`--output=${outputPath}`] : []),
+ ]
: [`--config=${stackPath}`];
const exitCode = await runCliPassthrough({
cwd,
diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts
index c15724b6..dfcb9000 100644
--- a/packages/cli/src/utils/constants.ts
+++ b/packages/cli/src/utils/constants.ts
@@ -5,6 +5,8 @@ export interface AdapterMeta {
label: string;
packageName: string;
ormForGenerate?: "prisma" | "drizzle" | "kysely";
+ /** Additional npm packages that must be installed when this adapter is selected. */
+ extraPackages?: string[];
}
export interface PluginMeta {
@@ -33,6 +35,7 @@ export const ADAPTERS: readonly AdapterMeta[] = [
label: "Prisma",
packageName: "@btst/adapter-prisma",
ormForGenerate: "prisma",
+ extraPackages: ["@prisma/adapter-pg", "pg"],
},
{
key: "drizzle",
diff --git a/packages/cli/src/utils/package-installer.ts b/packages/cli/src/utils/package-installer.ts
index ef93d33e..cf9b7b3d 100644
--- a/packages/cli/src/utils/package-installer.ts
+++ b/packages/cli/src/utils/package-installer.ts
@@ -39,6 +39,7 @@ export async function installInitDependencies(input: {
"@btst/yar",
"@tanstack/react-query",
adapterMeta.packageName,
+ ...(adapterMeta.extraPackages ?? []),
...pluginExtraPackages,
];
const { command, args } = getInstallCommand(input.packageManager, packages);
diff --git a/packages/cli/src/utils/passthrough.ts b/packages/cli/src/utils/passthrough.ts
index 8cbcc0c1..41fec26c 100644
--- a/packages/cli/src/utils/passthrough.ts
+++ b/packages/cli/src/utils/passthrough.ts
@@ -7,6 +7,15 @@ export function adapterNeedsGenerate(adapter: Adapter): boolean {
return Boolean(ADAPTERS.find((item) => item.key === adapter)?.ormForGenerate);
}
+export function getOutputForAdapter(adapter: Adapter): string | null {
+ const meta = ADAPTERS.find((item) => item.key === adapter);
+ if (!meta?.ormForGenerate) return null;
+
+ if (meta.ormForGenerate === "prisma") return "prisma/schema.prisma";
+ if (meta.ormForGenerate === "drizzle") return "src/db/schema.ts";
+ return "migrations/schema.sql";
+}
+
export function getGenerateHintForAdapter(
adapter: Adapter,
configPath: string,
@@ -14,12 +23,8 @@ export function getGenerateHintForAdapter(
const meta = ADAPTERS.find((item) => item.key === adapter);
if (!meta?.ormForGenerate) return null;
- const output =
- meta.ormForGenerate === "prisma"
- ? "schema.prisma"
- : meta.ormForGenerate === "drizzle"
- ? "src/db/schema.ts"
- : "migrations/schema.sql";
+ const output = getOutputForAdapter(adapter);
+ if (!output) return null;
return `npx @btst/codegen generate --orm=${meta.ormForGenerate} --config=${configPath} --output=${output}`;
}
diff --git a/packages/cli/src/utils/scaffold-plan.ts b/packages/cli/src/utils/scaffold-plan.ts
index 4183eb98..789f6825 100644
--- a/packages/cli/src/utils/scaffold-plan.ts
+++ b/packages/cli/src/utils/scaffold-plan.ts
@@ -322,7 +322,7 @@ function buildPluginTemplateContext(
};
}
-function buildAdapterTemplateContext(adapter: Adapter) {
+function buildAdapterTemplateContext(adapter: Adapter, stackPath: string) {
const meta = ADAPTERS.find((item) => item.key === adapter);
if (!meta) {
throw new Error(`Unsupported adapter: ${adapter}`);
@@ -337,15 +337,19 @@ function buildAdapterTemplateContext(adapter: Adapter) {
}
if (adapter === "prisma") {
+ const depth = stackPath.split("/").length - 1;
+ const prismaClientPath = `${"../".repeat(depth)}generated/prisma/client`;
return {
adapterImport: `import { createPrismaAdapter } from "${meta.packageName}"
-import { PrismaClient } from "@prisma/client"`,
- adapterSetup: `const prisma = new PrismaClient()
+import { PrismaClient } from "${prismaClientPath}"
+import { PrismaPg } from "@prisma/adapter-pg"`,
+ adapterSetup: `const pgAdapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! })
+const prisma = new PrismaClient({ adapter: pgAdapter })
-const provider = process.env.BTST_PRISMA_PROVIDER ?? "postgresql"
+const provider = (process.env.BTST_PRISMA_PROVIDER ?? "postgresql") as "postgresql" | "sqlite" | "cockroachdb" | "mysql" | "sqlserver" | "mongodb"
`,
adapterStackLine:
- "adapter: (db) => createPrismaAdapter(prisma, db, { provider }),",
+ "adapter: (db) => createPrismaAdapter(prisma, db, { provider })({}),",
};
}
@@ -385,7 +389,10 @@ export async function buildScaffoldPlan(
input.plugins,
input.framework,
);
- const adapterContext = buildAdapterTemplateContext(input.adapter);
+ const adapterContext = buildAdapterTemplateContext(
+ input.adapter,
+ frameworkPaths.stackPath,
+ );
const sharedContext = {
alias: input.alias,
@@ -402,6 +409,20 @@ export async function buildScaffoldPlan(
content: await renderTemplate("shared/lib/stack.ts.hbs", sharedContext),
description: "BTST backend stack configuration",
},
+ ...(input.adapter === "prisma"
+ ? [
+ {
+ path: "prisma/schema.prisma",
+ content: `generator client {\n provider = "prisma-client"\n output = "../generated/prisma"\n}\n\ndatasource db {\n provider = "postgresql"\n}\n`,
+ description: "Prisma schema with explicit client output path",
+ },
+ {
+ path: "prisma.config.ts",
+ content: `import { defineConfig } from 'prisma/config'\n\nexport default defineConfig({\n schema: 'prisma/schema.prisma',\n datasource: {\n url: process.env.DATABASE_URL ?? '',\n },\n})\n`,
+ description: "Prisma configuration file",
+ },
+ ]
+ : []),
{
path: frameworkPaths.stackClientPath,
content: await renderTemplate(
diff --git a/packages/stack/package.json b/packages/stack/package.json
index 1498d9e4..82f7b182 100644
--- a/packages/stack/package.json
+++ b/packages/stack/package.json
@@ -1,6 +1,6 @@
{
"name": "@btst/stack",
- "version": "2.11.3",
+ "version": "2.11.4",
"description": "A composable, plugin-based library for building full-stack applications.",
"repository": {
"type": "git",
diff --git a/packages/stack/src/plugins/cms/api/index.ts b/packages/stack/src/plugins/cms/api/index.ts
index 30426edb..2c24b752 100644
--- a/packages/stack/src/plugins/cms/api/index.ts
+++ b/packages/stack/src/plugins/cms/api/index.ts
@@ -15,5 +15,6 @@ export {
export {
createCMSContentItem,
type CreateCMSContentItemInput,
+ type CreateCMSContentItemOptions,
} from "./mutations";
export { CMS_QUERY_KEYS } from "./query-key-defs";
diff --git a/packages/stack/src/plugins/cms/api/mutations.ts b/packages/stack/src/plugins/cms/api/mutations.ts
index a690ccdf..1c5cf770 100644
--- a/packages/stack/src/plugins/cms/api/mutations.ts
+++ b/packages/stack/src/plugins/cms/api/mutations.ts
@@ -2,6 +2,11 @@ import type { DBAdapter as Adapter } from "@btst/db";
import type { ContentType, ContentItem } from "../types";
import { serializeContentItem } from "./getters";
import type { SerializedContentItem } from "../types";
+import {
+ collectExistingRelationIds,
+ extractRelationFields,
+ syncRelations,
+} from "./relations";
/**
* Input for creating a new CMS content item.
@@ -13,12 +18,37 @@ export interface CreateCMSContentItemInput {
data: Record;
}
+/**
+ * Options for {@link createCMSContentItem}.
+ */
+export interface CreateCMSContentItemOptions {
+ /**
+ * When `true`, persist relation fields (`belongsTo`, `hasMany`,
+ * `manyToMany`) into the `contentRelation` junction table in addition to
+ * the item's JSON payload.
+ *
+ * This is what the HTTP `POST /content/:typeSlug` route does, and is
+ * required for the admin "Related Items" panel / inverse-relation queries
+ * to find the item.
+ *
+ * Seeds and other programmatic callers that want the admin UI to work
+ * should enable this flag. Callers are still expected to pass
+ * pre-created target IDs (`{ id }` or `[{ id }, ...]`) — inline `_new`
+ * creation is only supported via the HTTP route.
+ *
+ * Defaults to `false` for backwards compatibility (a no-op for content
+ * types without relations).
+ */
+ syncRelations?: boolean;
+}
+
/**
* Create a new content item for a content type (looked up by slug).
*
- * Bypasses Zod schema validation and relation processing — the caller is
- * responsible for providing valid, relation-free data. For relation fields or
- * schema validation, use the HTTP endpoint instead.
+ * Bypasses Zod schema validation and inline `_new` relation creation — the
+ * caller is responsible for providing valid data and pre-created relation
+ * IDs. For schema validation or inline creation of related items, use the
+ * HTTP endpoint instead.
*
* Throws if the content type is not found or the slug is already taken within
* that content type.
@@ -30,11 +60,15 @@ export interface CreateCMSContentItemInput {
* @param adapter - The database adapter
* @param contentTypeSlug - Slug of the target content type
* @param input - Item slug and data payload
+ * @param options - See {@link CreateCMSContentItemOptions}. Pass
+ * `{ syncRelations: true }` to also populate the `contentRelation`
+ * junction table for relation fields in the payload.
*/
export async function createCMSContentItem(
adapter: Adapter,
contentTypeSlug: string,
input: CreateCMSContentItemInput,
+ options: CreateCMSContentItemOptions = {},
): Promise {
const contentType = await adapter.findOne({
model: "contentType",
@@ -80,5 +114,16 @@ export async function createCMSContentItem(
},
});
+ if (options.syncRelations) {
+ const relationFields = extractRelationFields(contentType);
+ if (Object.keys(relationFields).length > 0) {
+ const relationIds = collectExistingRelationIds(
+ input.data,
+ relationFields,
+ );
+ await syncRelations(adapter, item.id, relationIds);
+ }
+ }
+
return serializeContentItem(item);
}
diff --git a/packages/stack/src/plugins/cms/api/plugin.ts b/packages/stack/src/plugins/cms/api/plugin.ts
index 93f32a9d..30bc5e72 100644
--- a/packages/stack/src/plugins/cms/api/plugin.ts
+++ b/packages/stack/src/plugins/cms/api/plugin.ts
@@ -15,7 +15,6 @@ import type {
CMSBackendConfig,
CMSHookContext,
SerializedContentItemWithType,
- RelationConfig,
RelationValue,
InverseRelation,
} from "../types";
@@ -31,6 +30,12 @@ import {
serializeContentItemWithType,
} from "./getters";
import { createCMSContentItem } from "./mutations";
+import {
+ extractRelationFields,
+ isExistingRelationValue,
+ isNewRelationValue,
+ syncRelations,
+} from "./relations";
import type { QueryClient } from "@tanstack/react-query";
import { CMS_QUERY_KEYS } from "./query-key-defs";
import { runHookWithShim } from "../../utils";
@@ -145,67 +150,9 @@ function getContentTypeZodSchema(contentType: ContentType): z.ZodTypeAny {
}
// ========== Relation Helpers ==========
-
-interface JsonSchemaProperty {
- fieldType?: string;
- relation?: RelationConfig;
- type?: string;
- items?: JsonSchemaProperty;
- [key: string]: unknown;
-}
-
-interface JsonSchemaWithProperties {
- properties?: Record;
- [key: string]: unknown;
-}
-
-/**
- * Extract relation field configurations from a content type's JSON Schema
- */
-function extractRelationFields(
- contentType: ContentType,
-): Record {
- const jsonSchema = JSON.parse(
- contentType.jsonSchema,
- ) as JsonSchemaWithProperties;
- const properties = jsonSchema.properties || {};
- const relationFields: Record = {};
-
- for (const [fieldName, fieldSchema] of Object.entries(properties)) {
- if (fieldSchema.fieldType === "relation" && fieldSchema.relation) {
- relationFields[fieldName] = fieldSchema.relation;
- }
- }
-
- return relationFields;
-}
-
-/**
- * Check if a value is a "new" relation item (to be created)
- */
-function isNewRelationValue(
- value: unknown,
-): value is { _new: true; data: Record } {
- return (
- typeof value === "object" &&
- value !== null &&
- "_new" in value &&
- (value as { _new: unknown })._new === true &&
- "data" in value
- );
-}
-
-/**
- * Check if a value is an existing relation reference
- */
-function isExistingRelationValue(value: unknown): value is { id: string } {
- return (
- typeof value === "object" &&
- value !== null &&
- "id" in value &&
- typeof (value as { id: unknown }).id === "string"
- );
-}
+// `extractRelationFields`, `isNewRelationValue`, `isExistingRelationValue`,
+// and `syncRelations` live in ./relations so both this HTTP route module
+// and the programmatic mutations helper can share them.
/**
* Process relation fields in content data:
@@ -363,44 +310,6 @@ async function createRelatedItem(
return item;
}
-/**
- * Sync relations in the junction table for a content item.
- *
- * Only updates relations for fields explicitly present in relationIds.
- * Fields not in relationIds are left unchanged - this preserves existing
- * relations during partial updates.
- */
-async function syncRelations(
- adapter: Adapter,
- sourceId: string,
- relationIds: Record,
-): Promise {
- // Only sync fields that are explicitly included in relationIds
- for (const [fieldName, targetIds] of Object.entries(relationIds)) {
- // Delete existing relations for this specific field only
- await adapter.delete({
- model: "contentRelation",
- where: [
- { field: "sourceId", value: sourceId, operator: "eq" as const },
- { field: "fieldName", value: fieldName, operator: "eq" as const },
- ],
- });
-
- // Create new relations for this field
- for (const targetId of targetIds) {
- await adapter.create({
- model: "contentRelation",
- data: {
- sourceId,
- targetId,
- fieldName,
- createdAt: new Date(),
- },
- });
- }
- }
-}
-
/**
* Populate relations for a content item by fetching related items
*/
@@ -575,9 +484,10 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
createContentItem: async (
typeSlug: string,
input: Parameters[2],
+ options?: Parameters[3],
) => {
await ensureSynced(adapter);
- return createCMSContentItem(adapter, typeSlug, input);
+ return createCMSContentItem(adapter, typeSlug, input, options);
},
}),
diff --git a/packages/stack/src/plugins/cms/api/relations.ts b/packages/stack/src/plugins/cms/api/relations.ts
new file mode 100644
index 00000000..dd0a7720
--- /dev/null
+++ b/packages/stack/src/plugins/cms/api/relations.ts
@@ -0,0 +1,174 @@
+import type { DBAdapter as Adapter } from "@btst/db";
+import type {
+ ContentType,
+ ContentRelation,
+ RelationConfig,
+ RelationValue,
+} from "../types";
+
+/**
+ * Shape of a property inside a content type's stored JSON Schema.
+ * Relation fields carry `fieldType: "relation"` and a `relation` descriptor.
+ */
+interface JsonSchemaProperty {
+ fieldType?: string;
+ relation?: RelationConfig;
+ type?: string;
+ items?: JsonSchemaProperty;
+ [key: string]: unknown;
+}
+
+interface JsonSchemaWithProperties {
+ properties?: Record;
+ [key: string]: unknown;
+}
+
+/**
+ * Extract relation field configurations from a content type's JSON Schema.
+ *
+ * Returns a map of field name -> RelationConfig for every field declared
+ * with `fieldType: "relation"` and a populated `relation` descriptor.
+ */
+export function extractRelationFields(
+ contentType: ContentType,
+): Record {
+ const jsonSchema = JSON.parse(
+ contentType.jsonSchema,
+ ) as JsonSchemaWithProperties;
+ const properties = jsonSchema.properties || {};
+ const relationFields: Record = {};
+
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
+ if (fieldSchema.fieldType === "relation" && fieldSchema.relation) {
+ relationFields[fieldName] = fieldSchema.relation;
+ }
+ }
+
+ return relationFields;
+}
+
+/**
+ * Type guard: value is a `{ _new: true, data: ... }` descriptor used by the
+ * HTTP endpoints to request inline creation of a related item on save.
+ */
+export function isNewRelationValue(
+ value: unknown,
+): value is { _new: true; data: Record } {
+ return (
+ typeof value === "object" &&
+ value !== null &&
+ "_new" in value &&
+ (value as { _new: unknown })._new === true &&
+ "data" in value
+ );
+}
+
+/**
+ * Type guard: value is an existing relation reference (`{ id: string }`).
+ */
+export function isExistingRelationValue(
+ value: unknown,
+): value is { id: string } {
+ return (
+ typeof value === "object" &&
+ value !== null &&
+ "id" in value &&
+ typeof (value as { id: unknown }).id === "string"
+ );
+}
+
+/**
+ * Collect relation target IDs out of a content item's data payload,
+ * grouped by relation field name.
+ *
+ * Only processes fields that are:
+ * - present in `data`
+ * - declared as relations in `relationFields`
+ * - hold existing `{ id }` references (or arrays of them)
+ *
+ * Unlike `processRelationsInData` in the HTTP route, this does NOT create
+ * inline `_new` items — callers passing `_new` values will have them
+ * silently ignored. Programmatic callers (seeds, imports) are expected to
+ * supply pre-created IDs.
+ *
+ * Returns a map of `{ fieldName: targetIds[] }` ready to hand to
+ * {@link syncRelations}.
+ */
+export function collectExistingRelationIds(
+ data: Record,
+ relationFields: Record,
+): Record {
+ const relationIds: Record = {};
+
+ for (const [fieldName, relationConfig] of Object.entries(relationFields)) {
+ if (!(fieldName in data)) continue;
+
+ const fieldValue = data[fieldName];
+ if (!fieldValue) {
+ relationIds[fieldName] = [];
+ continue;
+ }
+
+ const ids: string[] = [];
+
+ if (relationConfig.type === "belongsTo") {
+ const value = fieldValue as RelationValue;
+ if (isExistingRelationValue(value)) {
+ ids.push(value.id);
+ }
+ } else {
+ // hasMany / manyToMany — expect an array of { id }
+ const values = (
+ Array.isArray(fieldValue) ? fieldValue : []
+ ) as RelationValue[];
+
+ for (const value of values) {
+ if (isExistingRelationValue(value)) {
+ ids.push(value.id);
+ }
+ }
+ }
+
+ relationIds[fieldName] = ids;
+ }
+
+ return relationIds;
+}
+
+/**
+ * Sync relations in the junction table for a content item.
+ *
+ * Only updates relations for fields explicitly present in `relationIds`.
+ * Fields not included are left untouched — this preserves existing
+ * relations during partial updates.
+ *
+ * For each included field, existing junction rows (matching sourceId +
+ * fieldName) are deleted and replaced with the provided target IDs.
+ */
+export async function syncRelations(
+ adapter: Adapter,
+ sourceId: string,
+ relationIds: Record,
+): Promise {
+ for (const [fieldName, targetIds] of Object.entries(relationIds)) {
+ await adapter.delete({
+ model: "contentRelation",
+ where: [
+ { field: "sourceId", value: sourceId, operator: "eq" as const },
+ { field: "fieldName", value: fieldName, operator: "eq" as const },
+ ],
+ });
+
+ for (const targetId of targetIds) {
+ await adapter.create({
+ model: "contentRelation",
+ data: {
+ sourceId,
+ targetId,
+ fieldName,
+ createdAt: new Date(),
+ },
+ });
+ }
+ }
+}