@vtecx/vtecxdocument 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/docs/api/acl.md +68 -0
- package/docs/api/auth.md +153 -0
- package/docs/api/content.md +177 -0
- package/docs/api/counter.md +96 -0
- package/docs/api/data.md +243 -0
- package/docs/api/db.md +148 -0
- package/docs/api/group.md +151 -0
- package/docs/api/index.md +117 -0
- package/docs/api/notify.md +123 -0
- package/docs/api/oauth.md +100 -0
- package/docs/api/pdf.md +75 -0
- package/docs/api/session.md +170 -0
- package/docs/api/user.md +225 -0
- package/docs/api/util.md +198 -0
- package/docs/framework.md +882 -0
- package/docs/vtecxnext-api.md +259 -0
- package/package.json +24 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
# vte.cx フレームワーク仕様
|
|
2
|
+
|
|
3
|
+
vte.cx BaaS のデータモデル・アクセス制御・スキーマ設計に関するリファレンスです。
|
|
4
|
+
このドキュメントは vte.cx を使用するすべてのプロジェクトで横展開できます。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## データ構造
|
|
9
|
+
|
|
10
|
+
### URI 設計
|
|
11
|
+
|
|
12
|
+
`getFeed` は指定したパス以下の**エントリ一覧を返す**ため、1つのディレクトリに複数種類のデータを混在させてはならない。
|
|
13
|
+
|
|
14
|
+
#### NG パターン(データ混在)
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/crm/customer/0000000001
|
|
18
|
+
/crm/customer/contact/... ← 担当者が顧客と同じディレクトリ
|
|
19
|
+
/crm/customer/deal/... ← 商談が顧客と同じディレクトリ
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`getFeed('/crm/customer')` を呼ぶと顧客一覧だけでなく担当者・商談も返ってくる。
|
|
23
|
+
|
|
24
|
+
#### OK パターン(種類ごとに独立したディレクトリ)
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
/crm/customer/0000000001
|
|
28
|
+
/crm/customer/0000000001/contact ← 顧客Aの担当者専用
|
|
29
|
+
/crm/customer/0000000001/deal ← 顧客Aの商談専用
|
|
30
|
+
/crm/customer/0000000001/activity ← 顧客Aの対応履歴専用
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| クエリ | 返るデータ |
|
|
34
|
+
| --------------------------------------- | -------------------- |
|
|
35
|
+
| `getFeed('/crm/customer')` | 顧客一覧 |
|
|
36
|
+
| `getEntry('/crm/customer/{id}')` | 顧客データ(1件) |
|
|
37
|
+
| `getFeed('/crm/customer/{id}/contact')` | その顧客の担当者のみ |
|
|
38
|
+
| `getFeed('/crm/customer/{id}/deal')` | その顧客の商談のみ |
|
|
39
|
+
|
|
40
|
+
#### 設計ルール
|
|
41
|
+
|
|
42
|
+
1. **1ディレクトリ = 1エンティティ種別** — 異なるエンティティを同じディレクトリ配下に置かない
|
|
43
|
+
2. **親子関係はパス階層で表現** — 子エンティティのパスに親IDを含める(例: `/crm/contact/{customerId}/{cid}`)
|
|
44
|
+
3. **folderacls はエンティティ種別ごとに登録** — データを置くルートパスをすべて登録する
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### 戻り値の型
|
|
49
|
+
|
|
50
|
+
SDK メソッドごとに戻り値の型が異なる。`feed.entry` は SDK 内部で解決されるため、呼び出し側は意識しなくてよい。
|
|
51
|
+
|
|
52
|
+
| メソッド | 正常時の型 | 204(データなし) |
|
|
53
|
+
| ---------- | ------------------ | -------------------- |
|
|
54
|
+
| `getFeed` | `VtecxApp.Entry[]` | `null` / `undefined` |
|
|
55
|
+
| `getEntry` | `VtecxApp.Entry` | `null` / `undefined` |
|
|
56
|
+
|
|
57
|
+
エラー時は `{ feed: { title: string } }` 型が返る。
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const entries = await vtecxnext.getFeed('/crm/customer')
|
|
61
|
+
// entries は VtecxApp.Entry[](配列)。Array.isArray による正規化は不要
|
|
62
|
+
|
|
63
|
+
const entry = await vtecxnext.getEntry('/crm/customer/0000000001')
|
|
64
|
+
// entry は VtecxApp.Entry(単一オブジェクト)または null(204)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### entry.id の形式
|
|
70
|
+
|
|
71
|
+
`entry.id` は `{パス},{リビジョン}` の形式で返る。パスを取り出す場合は `,` より前の部分のみ使う。
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// NG: リビジョン番号が混入する
|
|
75
|
+
const id = entry.id.replace('/crm/customer/', '') // → "0000000001,3"
|
|
76
|
+
|
|
77
|
+
// OK: パス部分のみ取り出す
|
|
78
|
+
const id = entry.id.split(',')[0].replace('/crm/customer/', '') // → "0000000001"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## データ取得
|
|
84
|
+
|
|
85
|
+
### 使い分け
|
|
86
|
+
|
|
87
|
+
| 状況 | 使うメソッド |
|
|
88
|
+
| --------------------------------------- | ------------------------------- |
|
|
89
|
+
| 件数が少なく全件取得で問題ない一覧 | `getFeed(uri)` |
|
|
90
|
+
| 件数が多くページング必要な一覧 | `getPageWithPagination(uri, n)` |
|
|
91
|
+
| 一覧の中から1件を取得(詳細・編集画面) | `getEntry(uri)` |
|
|
92
|
+
|
|
93
|
+
**基本ルール:**
|
|
94
|
+
|
|
95
|
+
- 一覧データの取得はページングを前提に行う(`getPageWithPagination`)
|
|
96
|
+
- 一覧からデータを1件選んで取得する場合は、必ずデータキー(URI)を使って `getEntry` で取得する
|
|
97
|
+
- ページング不要な少量データ(マスタ・サブリストなど)は `getFeed` でよい
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// ページング一覧(顧客一覧など)
|
|
101
|
+
const entries = await vtecxnext.getPageWithPagination('/crm/customer?l=25', n)
|
|
102
|
+
|
|
103
|
+
// ページング不要一覧(件数が少ないサブリストなど)
|
|
104
|
+
const feed = await vtecxnext.getFeed('/crm/contact/0000000001')
|
|
105
|
+
|
|
106
|
+
// 1件取得(一覧→詳細遷移時、編集画面など)
|
|
107
|
+
const entry = await vtecxnext.getEntry('/crm/customer/0000000001')
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### メソッド一覧
|
|
113
|
+
|
|
114
|
+
| メソッド | 用途 |
|
|
115
|
+
| ------------------------------- | --------------------------------------------------------- |
|
|
116
|
+
| `getFeed(uri)` | ページング不要な一覧取得 |
|
|
117
|
+
| `getEntry(uri)` | データキーを指定して1件取得 |
|
|
118
|
+
| `getPageWithPagination(uri, n)` | ページング一覧取得(`pagination` + `getPage` を一括処理) |
|
|
119
|
+
| `pagination(uri, pagerange)` | カーソルリスト(pageindex)を作成 |
|
|
120
|
+
| `getPage(uri, n)` | 指定ページのデータを取得(カーソルリストが必要) |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### ページング
|
|
125
|
+
|
|
126
|
+
vte.cx のページングは **カーソルリスト(pageindex)** を事前に作成してから、ページ単位でデータを取得する2ステップ方式を取る。
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
① pagination() ─ 取得キー(URI)とページ範囲を指定してカーソルリストを作成
|
|
130
|
+
② getPage() ─ 作成済みカーソルを使って指定ページのデータを取得
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`getPageWithPagination(uri, n)` はこの2ステップを内部で自動処理する。n=1 のとき自動的に `pagination()` を呼び出す。データが存在しない場合は `undefined` を返す。
|
|
134
|
+
|
|
135
|
+
#### URI パラメータ
|
|
136
|
+
|
|
137
|
+
| パラメータ | 指定箇所 | 説明 |
|
|
138
|
+
| ----------------------- | ------------------------- | ----------------------------- |
|
|
139
|
+
| `l=件数` | URI クエリ | 1ページ当たりの表示件数 |
|
|
140
|
+
| `n=ページ番号` | `getPage()` が自動付加 | 取得するページ番号(1始まり) |
|
|
141
|
+
| `_pagination=start,end` | `pagination()` が自動付加 | カーソルを作成するページ範囲 |
|
|
142
|
+
|
|
143
|
+
> `pagination()` と `getPage()` に渡す URI は完全一致である必要がある(検索条件も含めて同一にすること)。
|
|
144
|
+
|
|
145
|
+
#### pagerange
|
|
146
|
+
|
|
147
|
+
`pagination(uri, pagerange)` の `pagerange` は `"開始ページ,終了ページ"` 形式。**endPage は 50 推奨**。
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
"1,50" ← ページ1〜50 のカーソルを一括作成(推奨)
|
|
151
|
+
"51,100" ← ページ51〜100 を追加作成(続きがある場合)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### 実装例
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const n = parseInt(vtecxnext.getParameter('n') ?? '1', 10)
|
|
158
|
+
const uri = `/crm/customer?l=25`
|
|
159
|
+
|
|
160
|
+
const entries = await vtecxnext.getPageWithPagination(uri, n)
|
|
161
|
+
return vtecxnext.response(200, entries ?? null)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### PaginationInfo
|
|
165
|
+
|
|
166
|
+
`pagination()` の戻り値。
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
type PaginationInfo = {
|
|
170
|
+
lastPageNumber: number // 作成済みカーソルの最終ページ番号(0 = データなし)
|
|
171
|
+
countWithinRange: number // 指定ページ範囲内のエントリ数
|
|
172
|
+
hasNext: boolean // true = 指定 endPage を超えるデータが存在する
|
|
173
|
+
isMemorysort: boolean // メモリソートモードかどうか
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| 値 | 意味 | 対処 |
|
|
178
|
+
| ---------------------- | ---------------------------- | ----------------------------------------------- |
|
|
179
|
+
| `lastPageNumber === 0` | データが1件もない | null または空を返す |
|
|
180
|
+
| `hasNext === true` | endPage 以降にもデータがある | 必要なら `pagination(uri, '51,100')` を追加実行 |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## アクセス制御
|
|
185
|
+
|
|
186
|
+
### 概念
|
|
187
|
+
|
|
188
|
+
vte.cx のアクセス制御はすべて **フレームワークが担う**。API ルートはアクセス制御を実装しない。
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
① クライアント(APIルート)がデータにアクセス権限を付与する
|
|
192
|
+
② フレームワークがそのアクセス権限を参照してアクセス制御を行う
|
|
193
|
+
③ アクセス権限がない場合はフレームワークが HTTP 403 を返す
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**API ルートではロールチェック・オーナーチェックを実装しない。** folderacls とエントリの contributor 設定がアクセス制御のすべてである。
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### folderacls.json
|
|
201
|
+
|
|
202
|
+
アプリが使用するデータパスは、事前に vte.cx サーバーに登録する必要がある。
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
[
|
|
206
|
+
{
|
|
207
|
+
"contributor": [
|
|
208
|
+
{ "uri": "urn:vte.cx:acl:/_group/$admin,CURD" },
|
|
209
|
+
{ "uri": "urn:vte.cx:acl:+,CURDE" }
|
|
210
|
+
],
|
|
211
|
+
"link": [{ "___rel": "self", "___href": "/your/path" }]
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### 主体と権限
|
|
217
|
+
|
|
218
|
+
| 主体 | 表記 | 用途 |
|
|
219
|
+
| ---------------- | ---------------- | ----------------------------------------- |
|
|
220
|
+
| システム管理者 | `/_group/$admin` | サービス作成者。必ず `CURD` を付与する |
|
|
221
|
+
| ログインユーザー | `+` | 認証済みユーザー全員。通常 `CURDE` を付与 |
|
|
222
|
+
| 全員(匿名含む) | `*` | 公開データ(`R` のみ推奨) |
|
|
223
|
+
| カスタムグループ | `/_group/{name}` | 独自ロール(例: `/_group/sales`) |
|
|
224
|
+
|
|
225
|
+
| 権限 | 意味 |
|
|
226
|
+
| ---- | ------------------------------------ |
|
|
227
|
+
| `C` | 作成(POST) |
|
|
228
|
+
| `R` | 読取(GET) |
|
|
229
|
+
| `U` | 更新(PUT) |
|
|
230
|
+
| `D` | 削除(DELETE) |
|
|
231
|
+
| `E` | API 経由のみ許可(直接アクセス禁止) |
|
|
232
|
+
|
|
233
|
+
- `/_group/$admin` には必ず `CURD` を付与する(`E` を付けると直接アクセスが制限される)
|
|
234
|
+
- ログインユーザー(`+`)には `CURDE` を付与し、API 経由のみに制限する
|
|
235
|
+
- 子パスを登録する場合、親パスも必ず登録する
|
|
236
|
+
|
|
237
|
+
#### システムディレクトリ
|
|
238
|
+
|
|
239
|
+
先頭が `_` のパス(`/_group`、`/_html` など)はシステムディレクトリで、サービス作成時にフレームワークが自動生成する。folderacls.json への定義は不要。
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### contributor によるアクセス権限付与
|
|
244
|
+
|
|
245
|
+
エントリへのアクセス権限は `contributor` フィールドで設定する。フォーマットは folderacls.json と同一。
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
urn:vte.cx:acl:{グループキーまたはUID},{権限}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const uid = await vtecxnext.uid()
|
|
253
|
+
const entry = {
|
|
254
|
+
link: [{ ___rel: 'self', ___href: '/crm/customer/0000000001' }],
|
|
255
|
+
customer: { ... },
|
|
256
|
+
contributor: [
|
|
257
|
+
{ uri: 'urn:vte.cx:acl:/_group/$admin,CURD' },
|
|
258
|
+
{ uri: `urn:vte.cx:acl:${uid},CURD` },
|
|
259
|
+
{ uri: 'urn:vte.cx:acl:/_group/viewer,R' },
|
|
260
|
+
],
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
エントリを更新する際は `existing.contributor` を引き継ぐことで、元のアクセス権限が失われない。
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const existing = await vtecxnext.getEntry(uri)
|
|
268
|
+
const entry = {
|
|
269
|
+
link: [{ ___rel: 'self', ___href: uri }],
|
|
270
|
+
id: existing?.id,
|
|
271
|
+
myData: { ... },
|
|
272
|
+
contributor: existing?.contributor,
|
|
273
|
+
}
|
|
274
|
+
await vtecxnext.put({ feed: { entry: [entry] } })
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### 403 レスポンスへの対応
|
|
280
|
+
|
|
281
|
+
フレームワークが 403 を返した場合、クライアントはセッション切れまたは権限不足として処理する。
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
if (error?.status === 403) {
|
|
285
|
+
router.push('/login')
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## スキーマ(template.xml)
|
|
292
|
+
|
|
293
|
+
### 構文
|
|
294
|
+
|
|
295
|
+
エンティティのフィールドは `<content>` 内に DSL で定義する。
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
エンティティ名
|
|
299
|
+
フィールド名(型){最大値}
|
|
300
|
+
フィールド名(型)!
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- 1スペースインデントで子フィールド(ネスト)
|
|
304
|
+
- `!` で必須フィールド、`{n}` で string の最大文字数・数値の最大値
|
|
305
|
+
|
|
306
|
+
#### 型
|
|
307
|
+
|
|
308
|
+
| 型 | 用途 |
|
|
309
|
+
| ------------------ | ---------------------- |
|
|
310
|
+
| `string` | 文字列 |
|
|
311
|
+
| `int` | 整数 |
|
|
312
|
+
| `long` | 大きな整数(金額など) |
|
|
313
|
+
| `date` | 日付(ISO 8601) |
|
|
314
|
+
| `boolean` | 真偽値 |
|
|
315
|
+
| `float` / `double` | 小数 |
|
|
316
|
+
|
|
317
|
+
### スキーマ変更の制約
|
|
318
|
+
|
|
319
|
+
- **フィールドの削除は不可** — 一度定義したフィールドは恒久的に残る
|
|
320
|
+
- **追加フィールドは必ず末尾に追記する** — 途中に挿入するとスキーマが壊れる
|
|
321
|
+
- **`entry.title`, `entry.subtitle` は使用しない** — 全フィールドを専用スキーマで定義する
|
|
322
|
+
- **フィールド名は `snake_case`** — 小文字英字・数字・アンダースコアのみ(先頭は小文字英字)
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
// ❌ NG: 既存フィールドの途中に挿入
|
|
326
|
+
activity
|
|
327
|
+
next_action(string){500}
|
|
328
|
+
next_action_date(date) ← 途中に挿入
|
|
329
|
+
contact_uri(string){500} ← 既存フィールド
|
|
330
|
+
|
|
331
|
+
// ✅ OK: 既存フィールドをすべて保持し、末尾に追記
|
|
332
|
+
activity
|
|
333
|
+
next_action(string){500}
|
|
334
|
+
contact_uri(string){500} ← 既存フィールドはそのまま
|
|
335
|
+
next_action_date(date) ← 末尾に追加
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### template.xml ファイル全体の構造
|
|
339
|
+
|
|
340
|
+
```xml
|
|
341
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
342
|
+
<feed>
|
|
343
|
+
<entry>
|
|
344
|
+
<content>customer
|
|
345
|
+
name(string){255}
|
|
346
|
+
address(string){500}
|
|
347
|
+
phone(string){20}
|
|
348
|
+
status(string){20}
|
|
349
|
+
deal
|
|
350
|
+
name(string){255}
|
|
351
|
+
amount(long)
|
|
352
|
+
probability(int){100}
|
|
353
|
+
scheduled_date(date)
|
|
354
|
+
</content>
|
|
355
|
+
<link href="/_settings/template" rel="self"></link>
|
|
356
|
+
<rights>
|
|
357
|
+
customer.status:/crm/customer
|
|
358
|
+
customer.assigned_uid:/crm/customer
|
|
359
|
+
deal.stage:/crm/deal
|
|
360
|
+
name;/crm/customer|/crm/deal
|
|
361
|
+
password#
|
|
362
|
+
</rights>
|
|
363
|
+
</entry>
|
|
364
|
+
<entry>
|
|
365
|
+
<link href="/_settings/template_property" rel="self"/>
|
|
366
|
+
</entry>
|
|
367
|
+
</feed>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### インデックス設定(`<rights>` タグ)
|
|
373
|
+
|
|
374
|
+
フィールドに検索インデックスを設定することで、そのフィールドを使った絞り込み・ソートが可能になる。
|
|
375
|
+
|
|
376
|
+
#### 構文
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
{フィールドパス}:{フォルダパスの正規表現}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
| 構文 | 種類 | 用途 |
|
|
383
|
+
| --------------------- | ---------------------- | -------------------------------- |
|
|
384
|
+
| `field:path` | 通常インデックス | 完全一致検索・ソート・範囲検索 |
|
|
385
|
+
| `field;path` | 全文検索インデックス | 部分一致検索(`like` 検索) |
|
|
386
|
+
| `field1\|field2;path` | 複数フィールド全文検索 | 複数フィールドをまとめて全文検索 |
|
|
387
|
+
| `field:path=role` | フィールド ACL | 指定ロールのみ読み書き可 |
|
|
388
|
+
| `field#` | 暗号化 | フィールド値をサーバー側で暗号化 |
|
|
389
|
+
|
|
390
|
+
#### 注意事項
|
|
391
|
+
|
|
392
|
+
- **同一フィールドへの複数パス設定は `|` で1行にまとめる**(別々の行に分けると "Already specified" エラー)
|
|
393
|
+
- インデックス追加後は必ず `pnpm upload:template` でサーバーに反映する
|
|
394
|
+
- **⚠️ インデックス設定前に登録済みのデータにはインデックスが適用されない** — 既存データを再 PUT する必要がある
|
|
395
|
+
- フォルダパスは正規表現のため `/crm/customer` は `/crm/customer` で始まるすべてのパスにマッチする
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
### インデックスを使った検索クエリ
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
/path?f&entity.field-eq-value&l=25
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`f` パラメータ必須。インデックス検索が有効なのは **先頭の検索条件パラメータのみ**(2番目以降はメモリフィルタ)。
|
|
406
|
+
|
|
407
|
+
| 演算子 | 意味 |
|
|
408
|
+
| ------ | ------------------------- |
|
|
409
|
+
| `-eq-` | 等しい(完全一致) |
|
|
410
|
+
| `-ne-` | 等しくない |
|
|
411
|
+
| `-lt-` | より小さい |
|
|
412
|
+
| `-le-` | 以下 |
|
|
413
|
+
| `-gt-` | より大きい |
|
|
414
|
+
| `-ge-` | 以上 |
|
|
415
|
+
| `-fm-` | 前方一致(LIKE 'value%') |
|
|
416
|
+
| `-bm-` | 後方一致(LIKE '%value') |
|
|
417
|
+
| `-rg-` | 正規表現 |
|
|
418
|
+
| `-ft-` | 全文検索 |
|
|
419
|
+
|
|
420
|
+
OR 条件: `?f&|field-eq-a&|field-eq-b`
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## ユーザーと UID
|
|
425
|
+
|
|
426
|
+
### UID の発行
|
|
427
|
+
|
|
428
|
+
ユーザーがサービスに登録(サインアップ)すると、vte.cx フレームワークが自動的に **UID** を発行する。同時にユーザー専用ディレクトリ `/_user/{uid}` が生成される(folderacls.json への定義は不要)。
|
|
429
|
+
|
|
430
|
+
### UID の取得
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
const vtecxnext = new VtecxNext(req)
|
|
434
|
+
const uid = await vtecxnext.uid()
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### ログイン中のユーザー情報の取得
|
|
438
|
+
|
|
439
|
+
ログイン中ユーザーの詳細情報は `/_user/{uid}` エントリから取得する。
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const uid = await vtecxnext.uid()
|
|
443
|
+
const userEntry = await vtecxnext.getEntry(`/_user/${uid}`)
|
|
444
|
+
const email: string = userEntry?.contributor?.[0]?.email ?? ''
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### ユーザーエントリの構造(`/_user/{uid}`)
|
|
448
|
+
|
|
449
|
+
| フィールド | 内容 |
|
|
450
|
+
| --- | --- |
|
|
451
|
+
| `title` | UID |
|
|
452
|
+
| `subtitle` | ニックネーム |
|
|
453
|
+
| `summary` | ステータス(`Activated` / `Interim`) |
|
|
454
|
+
| `contributor[0].email` | メールアドレス |
|
|
455
|
+
|
|
456
|
+
### アカウントの状態
|
|
457
|
+
|
|
458
|
+
| 状態 | summary 値 | ログイン |
|
|
459
|
+
| --- | --- | --- |
|
|
460
|
+
| 仮登録 | `Interim` | 不可(確認メール未クリック) |
|
|
461
|
+
| 本登録 | `Activated` | 可 |
|
|
462
|
+
|
|
463
|
+
### サービス名の取得
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
const serviceName = await vtecxnext.service()
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### ユーザー専用データの保存
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
const uid = await vtecxnext.uid()
|
|
473
|
+
const entry = {
|
|
474
|
+
link: [{ ___rel: 'self', ___href: `/crm/user/${uid}` }],
|
|
475
|
+
user: { displayName: 'John Doe', role: 'sales' },
|
|
476
|
+
}
|
|
477
|
+
await vtecxnext.put({ feed: { entry: [entry] } })
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## パスワード変更
|
|
483
|
+
|
|
484
|
+
vtecxnext のパスワード変更には **2 つのフロー** がある。いずれも内部で `vtecxnext.changepass()` を呼び出し、vte.cx の `PUT /_changephash` エンドポイントに送信する。
|
|
485
|
+
|
|
486
|
+
### SDK メソッド
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
vtecxnext.changepass(newpswd: string, oldpswd?: string, passresetToken?: string)
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
| 引数 | 説明 |
|
|
493
|
+
| --- | --- |
|
|
494
|
+
| `newpswd` | 新しいパスワード(ハッシュ済み) |
|
|
495
|
+
| `oldpswd` | 現在のパスワード(ハッシュ済み)。ログイン済みフローで使用 |
|
|
496
|
+
| `passresetToken` | パスワードリセットトークン。メールリセットフローで使用 |
|
|
497
|
+
|
|
498
|
+
パスワードは送信前に `getHashpass(password)`(`@vtecx/vtecxauth`)でハッシュする。
|
|
499
|
+
|
|
500
|
+
### フロー 1:メールリセット(未ログイン)
|
|
501
|
+
|
|
502
|
+
```
|
|
503
|
+
/forgot-password でメールアドレスを入力
|
|
504
|
+
→ vtecxnext.passreset() でリセットメール送信
|
|
505
|
+
→ メール内リンク(_passreset_token & _RXID 付き URL)をクリック
|
|
506
|
+
→ /change-password ページで新パスワードを入力
|
|
507
|
+
→ POST /api/changepass?_RXID={rxid} に { newpswd, passresetToken } を送信
|
|
508
|
+
→ vtecxnext.loginWithRxid(rxid) でセッション確立後
|
|
509
|
+
→ vtecxnext.changepass(newpswd, undefined, passresetToken) を実行
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### フロー 2:現在のパスワードで変更(ログイン済み)
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
/account/change-password を開く
|
|
516
|
+
→ 現在のパスワード・新しいパスワードを入力
|
|
517
|
+
→ POST /api/changepass に { newpswd, oldpswd } を送信(_RXID なし)
|
|
518
|
+
→ セッションクッキーで認証済みのまま
|
|
519
|
+
→ vtecxnext.changepass(newpswd, oldpswd) を実行
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### API ルートの振り分けロジック
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
const rxid = vtecxnext.getParameter('_RXID') ?? ''
|
|
526
|
+
if (rxid) {
|
|
527
|
+
await vtecxnext.loginWithRxid(rxid)
|
|
528
|
+
} else if (!data.oldpswd) {
|
|
529
|
+
return vtecxnext.response(401, { feed: { title: 'Authentication error.' } })
|
|
530
|
+
}
|
|
531
|
+
await vtecxnext.changepass(data.newpswd, data.oldpswd, data.passresetToken)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## グループ管理
|
|
537
|
+
|
|
538
|
+
### グループディレクトリの事前登録
|
|
539
|
+
|
|
540
|
+
グループ(`/_group/admin` など)は folderacls.json に定義し、`pnpm upload:folderacls` でサーバーに登録する。
|
|
541
|
+
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"contributor": [
|
|
545
|
+
{ "uri": "urn:vte.cx:acl:/_group/$admin,CURD" },
|
|
546
|
+
{ "uri": "urn:vte.cx:acl:+,CURDE" }
|
|
547
|
+
],
|
|
548
|
+
"link": [{ "___rel": "self", "___href": "/_group/admin" }]
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### メソッド一覧
|
|
553
|
+
|
|
554
|
+
| メソッド | 用途 |
|
|
555
|
+
| -------------------------------- | ---------------------------------------------- |
|
|
556
|
+
| `addGroup(group)` | 自分をグループに登録(グループがなければ作成) |
|
|
557
|
+
| `joinGroup(group)` | 管理者に招待されたグループへの参加確認 |
|
|
558
|
+
| `addGroupByAdmin(uids, group)` | 管理者が指定ユーザーをグループに追加 |
|
|
559
|
+
| `leaveGroup(group)` | グループから脱退 |
|
|
560
|
+
| `leaveGroupByAdmin(uids, group)` | 管理者がユーザーをグループから削除 |
|
|
561
|
+
| `isGroupMember(uri)` | ログインユーザーのグループ所属確認 |
|
|
562
|
+
| `getGroups()` | サービス内の全グループ一覧取得 |
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
await vtecxnext.addGroup('/_group/sales')
|
|
566
|
+
await vtecxnext.joinGroup('/_group/sales')
|
|
567
|
+
await vtecxnext.addGroupByAdmin([uid], '/_group/sales')
|
|
568
|
+
const isAdmin = await vtecxnext.isGroupMember('/_group/admin')
|
|
569
|
+
const groups = await vtecxnext.getGroups()
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## エイリアス(横断検索)
|
|
575
|
+
|
|
576
|
+
エイリアスは、同じエントリに対して複数のパスから参照する仕組みです。`link` 配列に `rel="alternate"` を追加することで設定します。
|
|
577
|
+
|
|
578
|
+
**エイリアスの主な用途は「逆引き」の実現です。** 例えば「あるユーザーが担当する顧客一覧」を取得したいとき、顧客側にインデックスを設けなくてもエイリアスパスで `getFeed` すれば顧客エントリが直接返ります。
|
|
579
|
+
|
|
580
|
+
```
|
|
581
|
+
一次パス(正引き): /crm/customer/{cid}/member/{uid} ← getFeed('/crm/customer/{cid}/member')
|
|
582
|
+
エイリアス(逆引き): /crm/member/{uid}/{cid} ← getFeed('/crm/member/{uid}')
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### 登録
|
|
586
|
+
|
|
587
|
+
エイリアスは別エントリを作るのではなく、**既存エントリの `link` 配列に `rel="alternate"` を追加**します。親パスの登録とエントリ更新を同一 `put()` にまとめます(親パスを先頭に配置すること)。
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
const customer = await vtecxnext.getEntry(`/crm/customer/${cid}`)
|
|
591
|
+
const existingLinks = (customer as any)?.link ?? []
|
|
592
|
+
|
|
593
|
+
await vtecxnext.put({
|
|
594
|
+
feed: {
|
|
595
|
+
entry: [
|
|
596
|
+
{ link: [{ ___rel: 'self', ___href: `/crm/member/${uid}` }], contributor }, // 親パス(先頭)
|
|
597
|
+
{
|
|
598
|
+
...(customer as any),
|
|
599
|
+
link: [...existingLinks, { ___rel: 'alternate', ___href: `/crm/member/${uid}/${cid}` }],
|
|
600
|
+
},
|
|
601
|
+
]
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### 逆引き検索
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
const feed = await vtecxnext.getFeed(`/crm/member/${uid}`)
|
|
610
|
+
// feed は VtecxApp.Entry[](顧客エントリが直接返る。別途 getEntry 不要)
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### 担当者一覧の取得
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
const customer = await vtecxnext.getEntry(`/crm/customer/${cid}`)
|
|
617
|
+
const memberUids = ((customer as any)?.link ?? [])
|
|
618
|
+
.filter((l: any) => l.___rel === 'alternate' && l.___href?.startsWith('/crm/member/'))
|
|
619
|
+
.map((l: any) => (l.___href as string).split('/')[3])
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### 削除
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
const customer = await vtecxnext.getEntry(`/crm/customer/${cid}`)
|
|
626
|
+
const updatedLinks = ((customer as any)?.link ?? [])
|
|
627
|
+
.filter((l: any) => !(l.___rel === 'alternate' && l.___href === `/crm/member/${uid}/${cid}`))
|
|
628
|
+
await vtecxnext.put({ feed: { entry: [{ ...(customer as any), link: updatedLinks }] } })
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### エイリアスパスの権限設定
|
|
632
|
+
|
|
633
|
+
エイリアスパスも `folderacls.json` に登録します。
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{ "contributor": [{ "uri": "urn:vte.cx:acl:/_group/$admin,CURD" }, { "uri": "urn:vte.cx:acl:+,CURDE" }],
|
|
637
|
+
"link": [{ "___rel": "self", "___href": "/crm/member" }] }
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## データ登録・更新
|
|
643
|
+
|
|
644
|
+
> **可能な限り `put()` の `feed.entry` にまとめて、API 呼び出しを1回にすること。**
|
|
645
|
+
> これは vte.cx を使った実装における最重要の設計・実装方針である。
|
|
646
|
+
|
|
647
|
+
| 観点 | 個別 PUT(複数回) | まとめた PUT(1回) |
|
|
648
|
+
| -------------- | ------------------------------------------ | -------------------- |
|
|
649
|
+
| パフォーマンス | N回のネットワーク往復 | 1回のみ |
|
|
650
|
+
| 原子性 | 途中でエラーが起きると中途半端な状態になる | 全件成功 or 全件失敗 |
|
|
651
|
+
| サーバー負荷 | リクエスト数が多い | 少ない |
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
// ✅ 複数エントリを1回の put() にまとめる
|
|
655
|
+
await vtecxnext.put({
|
|
656
|
+
feed: {
|
|
657
|
+
entry: [
|
|
658
|
+
{ link: [{ ___rel: 'self', ___href: '/crm/customer/001' }], customer: {...}, contributor },
|
|
659
|
+
{ link: [{ ___rel: 'self', ___href: '/crm/customer/002' }], customer: {...}, contributor },
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### ⚠️ 親子データは必ず親を先に配置する
|
|
666
|
+
|
|
667
|
+
親パスが存在しない状態で子エントリを登録しようとすると `Parent path is required` エラーになる。
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// ✅ 正しい順序: 親 → 子パス
|
|
671
|
+
await vtecxnext.put({
|
|
672
|
+
feed: {
|
|
673
|
+
entry: [
|
|
674
|
+
{ link: [{ ___rel: 'self', ___href: '/crm/customer/001' }], customer: {...}, contributor },
|
|
675
|
+
{ link: [{ ___rel: 'self', ___href: '/crm/customer/001/contact' }], contributor },
|
|
676
|
+
{ link: [{ ___rel: 'self', ___href: '/crm/customer/001/activity' }], contributor },
|
|
677
|
+
]
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
// ❌ 誤った順序: 子パスを先に置くと Parent path is required エラー
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## メール設定(properties.xml)
|
|
686
|
+
|
|
687
|
+
`setup/_settings/properties.xml` を編集して `pnpm upload:properties` を実行することで、ユーザー登録メールとパスワードリセットメールが有効になる。
|
|
688
|
+
|
|
689
|
+
### `/_settings/properties` — サーバー設定
|
|
690
|
+
|
|
691
|
+
```xml
|
|
692
|
+
<entry>
|
|
693
|
+
<link href="/_settings/properties" rel="self"/>
|
|
694
|
+
<rights>
|
|
695
|
+
_recaptcha.sitekey=your-recaptcha-site-key
|
|
696
|
+
_mail.user=apikey
|
|
697
|
+
_mail.password=SG.your-sendgrid-api-key
|
|
698
|
+
_mail.from=no-reply@your-domain.com
|
|
699
|
+
_mail.from.personal=Your Service Name
|
|
700
|
+
_mail.transport.protocol=smtps
|
|
701
|
+
_mail.smtp.host=smtp.sendgrid.net
|
|
702
|
+
_mail.smtp.port=587
|
|
703
|
+
_mail.smtp.auth=true
|
|
704
|
+
</rights>
|
|
705
|
+
</entry>
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
| キー | 説明 |
|
|
709
|
+
| --------------------- | -------------------------------------------- |
|
|
710
|
+
| `_recaptcha.sitekey` | reCAPTCHA v2 サイトキー |
|
|
711
|
+
| `_mail.user` | SMTPユーザー名(SendGrid は固定値 `apikey`) |
|
|
712
|
+
| `_mail.password` | SMTPパスワード(SendGrid APIキー) |
|
|
713
|
+
| `_mail.from` | 送信元メールアドレス |
|
|
714
|
+
| `_mail.from.personal` | 送信者表示名 |
|
|
715
|
+
| `_mail.smtp.host` | SMTPホスト(SendGrid: `smtp.sendgrid.net`) |
|
|
716
|
+
| `_mail.smtp.port` | SMTPポート(`587`) |
|
|
717
|
+
|
|
718
|
+
### `/_settings/adduser` — ユーザー登録確認メール
|
|
719
|
+
|
|
720
|
+
```xml
|
|
721
|
+
<entry>
|
|
722
|
+
<link href="/_settings/adduser" rel="self"/>
|
|
723
|
+
<title>ユーザ登録のお申込み確認</title>
|
|
724
|
+
<subtitle>text</subtitle>
|
|
725
|
+
<summary>本登録URLをクリックしてください。
|
|
726
|
+
|
|
727
|
+
${VTECXNEXT_URL}/signup-completion?_RXID=${RXID}
|
|
728
|
+
|
|
729
|
+
トップページ ${URL}</summary>
|
|
730
|
+
<content type="text/html"><![CDATA[<html>
|
|
731
|
+
<body>
|
|
732
|
+
<p>本登録URLをクリックしてください。</p>
|
|
733
|
+
<p><a href="${VTECXNEXT_URL}/signup-completion?_RXID=${RXID}">本登録はこちら</a></p>
|
|
734
|
+
<p>トップページ: <a href="${URL}">${URL}</a></p>
|
|
735
|
+
</body>
|
|
736
|
+
</html>]]></content>
|
|
737
|
+
</entry>
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### `/_settings/passreset` — パスワードリセットメール
|
|
741
|
+
|
|
742
|
+
```xml
|
|
743
|
+
<entry>
|
|
744
|
+
<link href="/_settings/passreset" rel="self"/>
|
|
745
|
+
<title>パスワード変更</title>
|
|
746
|
+
<subtitle>text</subtitle>
|
|
747
|
+
<summary>以下のURLをクリックしてパスワード変更を行ってください。
|
|
748
|
+
|
|
749
|
+
${VTECXNEXT_URL}/change-password?${PASSRESET_TOKEN}</summary>
|
|
750
|
+
<content type="text/html"><![CDATA[<html>
|
|
751
|
+
<body>
|
|
752
|
+
<p>以下のURLをクリックしてパスワード変更を行ってください。</p>
|
|
753
|
+
<p><a href="${VTECXNEXT_URL}/change-password?${PASSRESET_TOKEN}">パスワード変更はこちら</a></p>
|
|
754
|
+
</body>
|
|
755
|
+
</html>]]></content>
|
|
756
|
+
</entry>
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
| 変数 | 内容 |
|
|
760
|
+
| -------------------- | ---------------------------------------------------------------- |
|
|
761
|
+
| `${VTECXNEXT_URL}` | アプリのベースURL(`.env.local` の `NEXT_PUBLIC_VTECXNEXT_URL`) |
|
|
762
|
+
| `${RXID}` | 本登録トークン(フレームワークが自動生成) |
|
|
763
|
+
| `${PASSRESET_TOKEN}` | パスワードリセットトークン(フレームワークが自動生成) |
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## 管理画面
|
|
768
|
+
|
|
769
|
+
管理画面 URL: https://admin.vte.cx/index.html
|
|
770
|
+
|
|
771
|
+
> **各サービスの管理画面へのアクセス手順:** https://admin.vte.cx にログイン → サービス一覧から該当サービスの「管理画面」ボタンを押下 → そのサービスの管理画面(エンドポイント管理・ユーザー管理など)が開く。
|
|
772
|
+
|
|
773
|
+
### エンドポイント管理
|
|
774
|
+
|
|
775
|
+
`folderacls.json` に相当するディレクトリパスとアクセス権限を管理する。`pnpm upload:folderacls` でアップロードした後、管理画面でも確認・追加編集が可能。
|
|
776
|
+
|
|
777
|
+
### スキーマ管理
|
|
778
|
+
|
|
779
|
+
`template.xml` のアップロードで作成されたスキーマを管理画面から確認できる。インデックス・フィールド ACL・暗号化の設定は `template.xml` の `<rights>` タグに記述する。
|
|
780
|
+
|
|
781
|
+
### データ確認
|
|
782
|
+
|
|
783
|
+
登録したデータはエンドポイント管理から参照できる。
|
|
784
|
+
|
|
785
|
+
1. https://admin.vte.cx/index.html にログイン
|
|
786
|
+
2. サービス一覧から対象サービスの「管理画面」を押下
|
|
787
|
+
3. 左メニューの「エンドポイント管理」をクリック
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## よくあるエラー
|
|
792
|
+
|
|
793
|
+
### Parent path is required.
|
|
794
|
+
|
|
795
|
+
```
|
|
796
|
+
VtecxNextError: Parent path is required. /your/path
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**原因**: 登録しようとしたパスの親ディレクトリが vte.cx サーバーに存在しない。
|
|
800
|
+
|
|
801
|
+
**対処**: `folderacls.json` に不足している親パスを追加して `pnpm upload:folderacls` を実行する。
|
|
802
|
+
|
|
803
|
+
例: `/crm/user/${uid}` への登録が失敗する場合 → `/crm` と `/crm/user` の両方が必要。
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
### Key is required.
|
|
808
|
+
|
|
809
|
+
```
|
|
810
|
+
VtecxNextError: Key is required.
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
**原因**: `put()` で送信するエントリに `link` が指定されていない。
|
|
814
|
+
|
|
815
|
+
**対処**: エントリに `link` を明示する。
|
|
816
|
+
|
|
817
|
+
```typescript
|
|
818
|
+
const entry = {
|
|
819
|
+
link: [{ ___rel: 'self', ___href: '/crm/user/uid123' }],
|
|
820
|
+
user: { ... }
|
|
821
|
+
}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
#### `id` フィールドについて
|
|
825
|
+
|
|
826
|
+
| フィールド | 用途 | 形式 | 必須 |
|
|
827
|
+
| --------------------------- | -------------------------- | -------------------------------- | ---------- |
|
|
828
|
+
| `link[___rel=self].___href` | リソースのパス(キー)指定 | `/path/to/entry` | PUT で必須 |
|
|
829
|
+
| `id` | 楽観的排他制御(競合判定) | `{path},{revision}` 例: `/foo,3` | 任意 |
|
|
830
|
+
|
|
831
|
+
- `id` を含めると、サーバーが現在のリビジョンと照合し、不一致の場合 **409 Conflict** を返す
|
|
832
|
+
- `id` を省略すると競合チェックなしで強制上書き
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
### 403 Forbidden
|
|
837
|
+
|
|
838
|
+
**原因**: アクセス権限がない、またはセッションが切れている。
|
|
839
|
+
|
|
840
|
+
**対処**:
|
|
841
|
+
|
|
842
|
+
- セッション切れ → ログイン画面へ自動遷移
|
|
843
|
+
- `folderacls.json` のパスに権限が付与されていない → 権限設定を見直して `pnpm upload:folderacls` を再実行
|
|
844
|
+
|
|
845
|
+
> localhost と `https://{サービス名}.vte.cx` はセッションが独立している。localhost で確認する場合は localhost でログインし直す必要がある。
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
849
|
+
### 401 Unauthorized
|
|
850
|
+
|
|
851
|
+
**原因**: 認証情報が間違っている(IDまたはパスワードの誤り)。
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
### 204 No Content
|
|
856
|
+
|
|
857
|
+
**意味**: エラーではなく、**対象データが存在しない**ことを示す正常レスポンス。
|
|
858
|
+
|
|
859
|
+
**対処**: エラーとして扱わず、「データなし」の状態として処理する。
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
const entry = await vtecxnext.getEntry('/crm/customer/0000000001')
|
|
863
|
+
if (entry == null) {
|
|
864
|
+
// データが存在しない(204)
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
### 409 Conflict
|
|
871
|
+
|
|
872
|
+
**原因**: `id`(リビジョン)が不一致(楽観的排他制御)。
|
|
873
|
+
|
|
874
|
+
**対処**: `getEntry` で最新取得し `id` を更新してから再 PUT する。
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
### Already specified
|
|
879
|
+
|
|
880
|
+
**原因**: 同一フィールドを複数行でインデックス設定している。
|
|
881
|
+
|
|
882
|
+
**対処**: `field:/path1|/path2` で1行にまとめる。
|