バックエンド連携

データベース設計・保存リクエストの受け方(save-request / save-result)・セキュリティ対策など、サーバー側で実装する内容です。

データベース構成

ZeroCode.jsのデータをRDBで管理する場合の推奨データベース設計です。

テーブル構成

以下の3つのテーブルで構成されます:

zcode_common_parts(共通パーツ)

すべての店舗で共有される共通パーツを管理します。

CREATE TABLE zcode_common_parts (
  id VARCHAR(50) PRIMARY KEY,
  type VARCHAR(50) NOT NULL,
  description TEXT,
  parts JSON NOT NULL,
  version INT NOT NULL DEFAULT 1,
  is_published BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
カラム名 型 NULL 説明
id VARCHAR(50) NO パーツタイプID(PK)
type VARCHAR(50) NO パーツタイプ(例: hero, features)
description TEXT YES タイプの説明
parts JSON NO パーツ配列(PartData[]形式)
version INT NO バージョン番号(楽観的ロック用)
is_published BOOLEAN NO 公開フラグ
created_at TIMESTAMP NO 作成日時
updated_at TIMESTAMP NO 更新日時

zcode_common_images(共通画像)

すべての店舗で共有される共通画像を管理します。

CREATE TABLE zcode_common_images (
  id VARCHAR(50) PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  url TEXT NOT NULL,
  mime_type VARCHAR(50) NOT NULL,
  needs_upload BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
カラム名 型 NULL 説明
id VARCHAR(50) NO 画像ID(PK)
name VARCHAR(255) NO 画像名
url TEXT NO 画像URL(base64の場合はdata:image/...形式)
mime_type VARCHAR(50) NO MIMEタイプ(例: image/jpeg, image/png)
needs_upload BOOLEAN NO アップロード要否(trueの場合、バックエンドでアップロード処理が必要)
created_at TIMESTAMP NO 作成日時
updated_at TIMESTAMP NO 更新日時

zcode_individual(店舗ごとの個別データ)

店舗ごとのページ、個別パーツ、個別画像を管理します。

CREATE TABLE zcode_individual (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  data_type ENUM('page', 'parts', 'images') NOT NULL,
  content JSON NOT NULL,
  version INT NOT NULL DEFAULT 1,
  status ENUM('draft', 'published', 'archived') NOT NULL DEFAULT 'draft',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_user_type_status (user_id, data_type, status)
);
カラム名 型 NULL 説明
id BIGINT NO レコードID(PK, AUTO_INCREMENT)
user_id BIGINT NO 店舗ID(外部キー)
data_type ENUM NO データ種別(page, parts, images)
content JSON NO データ内容(ComponentData[], TypeData[], ImageData[]形式)
version INT NO バージョン番号(楽観的ロック用)
status ENUM NO ステータス(draft: 下書き, published: 公開, archived: アーカイブ)
created_at TIMESTAMP NO 作成日時
updated_at TIMESTAMP NO 更新日時

データ例

zcode_individual のデータ例

-- 店舗1(user_id=1)のページデータ(下書き)
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
  1,
  'page',
  '[{"id": "hero-1", "part_id": "zcode-part-1", "title": "店舗1のタイトル", ...}]',
  'draft'
);

-- 店舗1(user_id=1)のページデータ(公開版)
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
  1,
  'page',
  '[{"id": "hero-1", "part_id": "zcode-part-1", "title": "店舗1のタイトル", ...}]',
  'published'
);

-- 店舗1(user_id=1)の個別パーツデータ
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
  1,
  'parts',
  '[{"id": "zcode-part-12", "type": "cta", "description": "店舗専用のCTA", ...}]',
  'published'
);

-- 店舗1(user_id=1)の個別画像データ
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
  1,
  'images',
  '[{"id": "img-ind-1", "name": "店舗専用画像", "url": "/images/store1-hero.jpg", ...}]',
  'published'
);

クエリ例

店舗の公開ページデータを取得

SELECT content
FROM zcode_individual
WHERE user_id = 1
  AND data_type = 'page'
  AND status = 'published'
LIMIT 1;

店舗の下書きページデータを取得

SELECT content
FROM zcode_individual
WHERE user_id = 1
  AND data_type = 'page'
  AND status = 'draft'
LIMIT 1;

共通パーツを取得

SELECT parts
FROM zcode_common_parts
WHERE is_published = TRUE;

設計の特徴

注意事項

保存リクエスト

保存ボタンをクリックすると、save-requestイベントが発火します。

イベントの受け取り方

event.detail に data は含まれません。cms.getData() で取得してください。

const cms = document.getElementById('cms');

cms.addEventListener('save-request', (event) => {
  const { requestId, source, targets, timestamp } = event.detail;
  const data = cms.getData();

  for (const target of targets) {
    fetch('/api/zero-code/save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ target, source, data, requestId, timestamp })
    });
  }
});

保存ターゲットの仕様

save-requestイベントのtargets配列には、現在のタブやモードに応じて複数のターゲットが含まれる場合があります。

タブ/モード 含まれるターゲット 説明
ページ管理(編集モード) ['page', 'images-special'] ページデータと専用画像プールを保存対象として通知(ホストが永続化方針を決定)
パーツ管理 ['parts-common', 'parts-common-css']、['parts-individual', 'parts-individual-css']、または['parts-special', 'parts-special-css'] パーツデータとページCSSを保存(パーツ編集時にページCSSも編集可能なため)
画像管理 ['images-common']、['images-individual']、または['images-special'] 画像データのみを保存
データビューアー 選択中のタブとカテゴリに応じて決定 現在表示中のデータに対応するターゲット

画像のアップロード

targets配列にimages-common、images-individual、またはimages-specialが含まれる場合、needsUpload: trueの画像をアップロード処理してください。data はハンドラ先頭で cms.getData() したもの。

for (const target of targets) {
  if (target.startsWith('images-')) {
    const images = target === 'images-common'
      ? data.images?.common || []
      : target === 'images-individual'
      ? data.images?.individual || []
      : data.images?.special || [];

    const imagesToUpload = images.filter(img => img.needsUpload === true);

    for (const image of imagesToUpload) {
      // base64データをデコード
      const base64Data = image.url.replace(/^data:image\/\w+;base64,/, '');
      const buffer = Buffer.from(base64Data, 'base64');

      // S3などにアップロード
      // アップロード後、image.urlを更新し、needsUploadをfalseに設定
    }
  }
}

バリデーションエラーの返却

バックエンドでバリデーションを行い、エラーがある場合はsave-resultイベントでエラーを返してください。フロントエンドでは、requiredとmaxのみをチェックします。複雑なバリデーション(メール形式チェック、重複チェックなど)はバックエンドで実施してください。

イベント形式:

// 成功時
cms.dispatchEvent(
  new CustomEvent('save-result', {
    detail: {
      requestId: 'req-1234567890-abc', // save-request の requestId
      target: 'page', // この回の保存対象(targets の要素のいずれか)
      ok: true,
      errors: []
    },
    bubbles: true,
    composed: true
  })
);

// エラー時
cms.dispatchEvent(
  new CustomEvent('save-result', {
    detail: {
      requestId: 'req-1234567890-abc', // save-request の requestId
      target: 'page', // この回の保存対象(targets の要素のいずれか)
      ok: false,
      errors: [
        {
          path: 'page.0', // コンポーネントのパス(オプション、指定しない場合はすべてのコンポーネントに適用)
          field: 'email', // フィールド名
          message: 'メールアドレスの形式が正しくありません', // エラーメッセージ
          code: 'FORMAT' // エラーコード(オプション)
        }
      ]
    },
    bubbles: true,
    composed: true
  })
);

実装例(Node.js/Express):

cms.addEventListener('save-request', async (event) => {
  const { requestId, source, targets, timestamp } = event.detail;
  const data = cms.getData();

  for (const target of targets) {
    try {
      // バリデーション
      const errors = [];

        if (target === 'page') {
          // ページデータのバリデーション
          const pageData = data.page || [];
          for (let i = 0; i < pageData.length; i++) {
            const component = pageData[i];

            // メール形式チェック(例)
            if (component.email && !component.email.includes('@')) {
              errors.push({
                path: `page.${i}`,
                field: 'email',
                message: 'メールアドレスの形式が正しくありません',
                code: 'FORMAT'
              });
            }

            // 重複チェック(例)
            if (component.title && await isTitleDuplicate(component.title)) {
              errors.push({
                path: `page.${i}`,
                field: 'title',
                message: 'このタイトルは既に使用されています',
                code: 'DUPLICATE'
              });
            }
          }
        } else if (target === 'parts-common-css' || target === 'parts-individual-css' || target === 'parts-special-css') {
          // CSSのバリデーション(例)
          const css = target === 'parts-common-css' ? data.css?.common :
                      target === 'parts-individual-css' ? data.css?.individual :
                      data.css?.special || '';
          if (css.length > 10000) {
            errors.push({
              field: target,
              message: 'CSSのサイズが大きすぎます',
              code: 'SIZE_LIMIT'
            });
          }
        } else if (target.startsWith('images-')) {
          // 画像データのアップロード処理
          const images = target === 'images-common'
            ? data.images?.common || []
            : target === 'images-individual'
            ? data.images?.individual || []
            : data.images?.special || [];

          for (const image of images) {
            if (image.needsUpload) {
              // 画像をアップロード
              const uploadedUrl = await uploadImage(image);
              image.url = uploadedUrl;
              image.needsUpload = false;
            }
          }
        }

        if (errors.length > 0) {
          // エラーがある場合
          cms.dispatchEvent(
            new CustomEvent('save-result', {
              detail: {
                requestId,
                target,
                ok: false,
                errors
              },
              bubbles: true,
              composed: true
            })
          );
          continue; // 次のターゲットへ
        }

        // データベースに保存
        await saveToDatabase(target, data);

        // 成功時
        cms.dispatchEvent(
          new CustomEvent('save-result', {
            detail: {
              requestId,
              target,
              ok: true,
              errors: []
            },
            bubbles: true,
            composed: true
          })
        );
      } catch (error) {
        // エラー時
        cms.dispatchEvent(
          new CustomEvent('save-result', {
            detail: {
              requestId,
              target,
              ok: false,
              errors: [
                {
                  field: 'general',
                  message: '保存に失敗しました',
                  code: 'SAVE_FAILED'
                }
              ]
            },
            bubbles: true,
            composed: true
          })
        );
      }
    }
  }
});

エラーの表示:

注意: requestIdはsave-requestイベントのrequestIdと同じ値を使用してください。これにより、複数の保存リクエストが同時に発火した場合でも、正しいレスポンスとリクエストを関連付けることができます。

セキュリティ

重要: ZeroCode.jsはフロントエンドライブラリのため、クライアント側での完全なセキュリティ保証はできません。サーバー側での検証を必ず実装してください。

セキュリティモデル(信頼境界)

ZeroCode.js は HTML を生成できる入力は信頼できる人に限ることを前提としています。

CSP(Content Security Policy)

ZeroCode.js は unsafe-eval を必要としません。CSP を設定する場合は、次を許可してください。

必須実装事項

1. サーバー側でのデータ検証(必須)

2. 認証・認可

3. 属性値のセキュリティ

4. パーツテンプレートの管理

実装例

※ クライアントは save-request ハンドラで cms.getData() し、targets をループして各 target と data を req.body に含めて送る。

// Node.js/Express の例(サーバー側)
app.post('/api/zero-code/save', authenticate, (req, res) => {
  const { target, source, data } = req.body;

  if (source === 'cms' && target.startsWith('parts-')) {
    return res.status(403).json({
      error: 'CMSからのパーツ保存は拒否されました'
    });
  }

  // データ構造の検証
  if (!validateDataStructure(data)) {
    return res.status(400).json({
      error: '無効なデータです'
    });
  }

  // パーツテンプレートの検証
  if (target.startsWith('parts-')) {
    if (!validatePartTemplate(data)) {
      return res.status(400).json({
        error: '無効なテンプレートです'
      });
    }
  }

  // 画像アップロード処理
  if (target.startsWith('images-')) {
    const imagesToUpload = data.filter(img => img.needsUpload === true);
    for (const image of imagesToUpload) {
      // アップロード処理
    }
  }

  // 保存処理
  // ...

  res.json({ success: true });
});

画像アップロードのセキュリティ