@squadbase/vite-server 0.1.17-dev.a9ddcfa → 0.1.17-dev.e4768d1

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.
Files changed (80) hide show
  1. package/dist/cli/index.js +8941 -3700
  2. package/dist/connectors/airtable-oauth.js +87 -23
  3. package/dist/connectors/airtable.js +130 -23
  4. package/dist/connectors/amplitude.js +47 -23
  5. package/dist/connectors/anthropic.js +41 -17
  6. package/dist/connectors/asana.js +76 -25
  7. package/dist/connectors/attio.js +69 -28
  8. package/dist/connectors/aws-billing.js +47 -23
  9. package/dist/connectors/azure-sql.js +322 -25
  10. package/dist/connectors/backlog-api-key.js +79 -30
  11. package/dist/connectors/clickup.js +89 -25
  12. package/dist/connectors/cosmosdb.js +51 -27
  13. package/dist/connectors/customerio.js +47 -23
  14. package/dist/connectors/dbt.js +725 -40
  15. package/dist/connectors/freshdesk.js +121 -23
  16. package/dist/connectors/freshsales.js +47 -23
  17. package/dist/connectors/freshservice.js +47 -23
  18. package/dist/connectors/gamma.js +54 -30
  19. package/dist/connectors/gemini.js +41 -17
  20. package/dist/connectors/github.js +51 -27
  21. package/dist/connectors/gmail-oauth.js +42 -23
  22. package/dist/connectors/gmail.js +43 -19
  23. package/dist/connectors/google-ads.js +47 -23
  24. package/dist/connectors/google-analytics-oauth.js +191 -40
  25. package/dist/connectors/google-analytics.js +529 -111
  26. package/dist/connectors/google-audit-log.js +43 -19
  27. package/dist/connectors/google-calendar-oauth.js +100 -30
  28. package/dist/connectors/google-calendar.js +100 -26
  29. package/dist/connectors/google-docs.js +42 -23
  30. package/dist/connectors/google-drive.js +64 -23
  31. package/dist/connectors/google-search-console-oauth.js +165 -32
  32. package/dist/connectors/google-sheets.js +142 -199
  33. package/dist/connectors/google-slides.js +42 -23
  34. package/dist/connectors/grafana.js +84 -25
  35. package/dist/connectors/hackernews.d.ts +5 -0
  36. package/dist/connectors/hackernews.js +909 -0
  37. package/dist/connectors/hubspot-oauth.js +73 -22
  38. package/dist/connectors/hubspot.js +64 -24
  39. package/dist/connectors/influxdb.js +47 -23
  40. package/dist/connectors/intercom-oauth.js +104 -25
  41. package/dist/connectors/intercom.js +51 -27
  42. package/dist/connectors/jdbc.js +76 -25
  43. package/dist/connectors/jira-api-key.js +107 -26
  44. package/dist/connectors/kintone-api-token.js +105 -33
  45. package/dist/connectors/kintone.js +182 -26
  46. package/dist/connectors/linear.js +93 -27
  47. package/dist/connectors/linkedin-ads.js +80 -29
  48. package/dist/connectors/mailchimp-oauth.js +45 -21
  49. package/dist/connectors/mailchimp.js +45 -21
  50. package/dist/connectors/meta-ads-oauth.js +72 -29
  51. package/dist/connectors/meta-ads.js +74 -29
  52. package/dist/connectors/mixpanel.js +47 -23
  53. package/dist/connectors/monday.js +48 -24
  54. package/dist/connectors/mongodb.js +47 -23
  55. package/dist/connectors/notion-oauth.js +92 -24
  56. package/dist/connectors/notion.js +144 -26
  57. package/dist/connectors/openai.js +41 -17
  58. package/dist/connectors/oracle.js +316 -26
  59. package/dist/connectors/outlook-oauth.js +53 -34
  60. package/dist/connectors/powerbi-oauth.js +45 -26
  61. package/dist/connectors/salesforce.js +150 -24
  62. package/dist/connectors/semrush.js +45 -21
  63. package/dist/connectors/sentry.js +75 -25
  64. package/dist/connectors/shopify-oauth.js +75 -23
  65. package/dist/connectors/shopify.js +47 -23
  66. package/dist/connectors/sqlserver.js +322 -25
  67. package/dist/connectors/stripe-api-key.js +105 -30
  68. package/dist/connectors/stripe-oauth.js +102 -32
  69. package/dist/connectors/supabase.js +315 -21
  70. package/dist/connectors/tableau.js +54 -30
  71. package/dist/connectors/tiktok-ads.js +76 -31
  72. package/dist/connectors/wix-store.js +47 -23
  73. package/dist/connectors/x.d.ts +5 -0
  74. package/dist/connectors/x.js +951 -0
  75. package/dist/connectors/zendesk-oauth.js +87 -25
  76. package/dist/connectors/zendesk.js +51 -27
  77. package/dist/index.js +9005 -3709
  78. package/dist/main.js +9005 -3709
  79. package/dist/vite-plugin.js +8939 -3698
  80. package/package.json +12 -4
@@ -1,6 +1,11 @@
1
1
  var __getOwnPropNames = Object.getOwnPropertyNames;
2
- var __esm = (fn, res) => function __init() {
3
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
2
+ var __esm = (fn, res, err) => function __init() {
3
+ if (err) throw err[0];
4
+ try {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ } catch (e) {
7
+ throw err = [e], e;
8
+ }
4
9
  };
5
10
 
6
11
  // ../connectors/src/parameter-definition.ts
@@ -169,6 +174,15 @@ var ConnectorPlugin = class _ConnectorPlugin {
169
174
  * `runSetupFlow` from `setup-flow.ts`.
170
175
  */
171
176
  setup;
177
+ /**
178
+ * 統一 introspection。SQL DB の `information_schema` / `SHOW` / `DESCRIBE` や SaaS の
179
+ * メタデータ API といった取得経路の差を吸収し、tables / columns / PK / FK を共通型
180
+ * {@link IntrospectedSchema} へ正規化して返す。取得経路は {@link IntrospectContext} で
181
+ * 抽象化され、SQL DB は `ctx.query`、SaaS は `ctx.proxyFetch` + `ctx.params` を使う。
182
+ * `ctx.profile === true` のときは L2 プロファイル(行数・NULL 率等)も best-effort で埋める。
183
+ * coding-agent の `DataContextIndex` がデータ地図を構築する際に使う。
184
+ */
185
+ introspect;
172
186
  /**
173
187
  * Opt-out of the default "verify before save" behavior on connection
174
188
  * creation. The backend invokes `checkConnection` synchronously while
@@ -183,6 +197,14 @@ var ConnectorPlugin = class _ConnectorPlugin {
183
197
  * the default verify-on-create behavior without opt-in.
184
198
  */
185
199
  skipConnectionCheckOnCreate;
200
+ /**
201
+ * 決定論的な「立ち上げ survey」レシピ(ルールベース prewarm 基盤)。実装した connector は、LLM を使わずに
202
+ * read-only で自分を survey する probe 列(テーブル/列メタデータ → 小サンプル)を宣言する。coding-agent の
203
+ * recon driver がこれを読み、connector ツールの `execute` を直接呼んで結果を seed にする。未実装の connector は
204
+ * driver 側で safely skip される(その connection は listConnections のみ seed)。標準 `information_schema` を
205
+ * 持つ SQL connector は `makeStandardSqlSurveyProbes` を使える。**probe は必ず read-only**。
206
+ */
207
+ surveyProbes;
186
208
  constructor(config) {
187
209
  this.slug = config.slug;
188
210
  this.authType = config.authType;
@@ -200,7 +222,9 @@ var ConnectorPlugin = class _ConnectorPlugin {
200
222
  this.query = config.query;
201
223
  this.checkConnection = config.checkConnection;
202
224
  this.setup = config.setup;
225
+ this.introspect = config.introspect;
203
226
  this.skipConnectionCheckOnCreate = config.skipConnectionCheckOnCreate;
227
+ this.surveyProbes = config.surveyProbes;
204
228
  }
205
229
  get connectorKey() {
206
230
  return _ConnectorPlugin.deriveKey(this.slug, this.authType);
@@ -208,7 +232,7 @@ var ConnectorPlugin = class _ConnectorPlugin {
208
232
  /**
209
233
  * Create tools for connections that belong to this connector.
210
234
  * Filters connections by connectorKey internally.
211
- * Returns tools keyed as `${connectorKey}_${toolName}`.
235
+ * Returns tools keyed as `connector_${connectorKey}_${toolName}`.
212
236
  */
213
237
  createTools(connections, config, opts) {
214
238
  const myConnections = connections.filter(
@@ -218,7 +242,7 @@ var ConnectorPlugin = class _ConnectorPlugin {
218
242
  for (const t of Object.values(this.tools)) {
219
243
  const tool = t.createTool(myConnections, config);
220
244
  const originalToModelOutput = tool.toModelOutput;
221
- result[`${this.connectorKey}_${t.name}`] = {
245
+ result[`connector_${this.connectorKey}_${t.name}`] = {
222
246
  ...tool,
223
247
  toModelOutput: async (options) => {
224
248
  if (!originalToModelOutput) {
@@ -238,25 +262,25 @@ var ConnectorPlugin = class _ConnectorPlugin {
238
262
  if (authType) return `${slug}-${authType}`;
239
263
  const LEGACY_NULL_AUTH_TYPE_MAP = {
240
264
  // user-password
241
- "postgresql": "user-password",
242
- "mysql": "user-password",
243
- "clickhouse": "user-password",
244
- "kintone": "user-password",
265
+ postgresql: "user-password",
266
+ mysql: "user-password",
267
+ clickhouse: "user-password",
268
+ kintone: "user-password",
245
269
  "squadbase-db": "user-password",
246
270
  // service-account
247
- "snowflake": "service-account",
248
- "bigquery": "service-account",
271
+ snowflake: "service-account",
272
+ bigquery: "service-account",
249
273
  "google-analytics": "service-account",
250
274
  "google-calendar": "service-account",
251
275
  "aws-athena": "service-account",
252
- "redshift": "service-account",
276
+ redshift: "service-account",
253
277
  // api-key
254
- "databricks": "api-key",
255
- "dbt": "api-key",
256
- "airtable": "api-key",
257
- "openai": "api-key",
258
- "gemini": "api-key",
259
- "anthropic": "api-key",
278
+ databricks: "api-key",
279
+ dbt: "api-key",
280
+ airtable: "api-key",
281
+ openai: "api-key",
282
+ gemini: "api-key",
283
+ anthropic: "api-key",
260
284
  "wix-store": "api-key"
261
285
  };
262
286
  const fallbackAuthType = LEGACY_NULL_AUTH_TYPE_MAP[slug];
@@ -265,6 +289,84 @@ var ConnectorPlugin = class _ConnectorPlugin {
265
289
  }
266
290
  };
267
291
 
292
+ // ../connectors/src/survey-probe.ts
293
+ function rowsOf(output) {
294
+ if (!output || typeof output !== "object") return [];
295
+ const o = output;
296
+ if (o.success === false) return [];
297
+ return Array.isArray(o.rows) ? o.rows : [];
298
+ }
299
+ function pickString(row, candidates) {
300
+ for (const key of candidates) {
301
+ const v = row[key] ?? row[key.toUpperCase()] ?? row[key.toLowerCase()];
302
+ if (typeof v === "string" && v.length > 0) return v;
303
+ }
304
+ return void 0;
305
+ }
306
+ var TABLE_NAME_FIELDS = [
307
+ "table_name",
308
+ "name",
309
+ "tableName",
310
+ "tab_name",
311
+ "Table",
312
+ "table"
313
+ ];
314
+ function makeQuoteIdent(opts) {
315
+ if (opts.bracket) {
316
+ return (name) => `[${name.replaceAll("]", "]]")}]`;
317
+ }
318
+ const q = opts.quote ?? '"';
319
+ return (name) => `${q}${name.replaceAll(q, q + q)}${q}`;
320
+ }
321
+ function sampleSql(ref, limit) {
322
+ return `SELECT * FROM (SELECT * FROM ${ref}) AS _sq LIMIT ${limit}`;
323
+ }
324
+ function distinctTables(prior, max) {
325
+ const names = [];
326
+ const seen = /* @__PURE__ */ new Set();
327
+ for (const row of rowsOf(prior[0]?.output)) {
328
+ const name = pickString(row, TABLE_NAME_FIELDS);
329
+ if (!name || seen.has(name)) continue;
330
+ seen.add(name);
331
+ names.push(name);
332
+ if (names.length >= max) break;
333
+ }
334
+ return names;
335
+ }
336
+ function makeStandardSqlSurveyProbes(opts) {
337
+ const queryTool = opts.queryTool ?? "executeQuery";
338
+ const sampleLimit = opts.sampleLimit ?? 5;
339
+ const maxSampleTables = opts.maxSampleTables ?? 4;
340
+ const quoteIdent = makeQuoteIdent(opts);
341
+ const whereSchema = opts.schemaLiteral ? ` WHERE table_schema = '${opts.schemaLiteral.replaceAll("'", "''")}'` : opts.schemaExpr ? ` WHERE table_schema = ${opts.schemaExpr}` : "";
342
+ return (_connectionId, prior) => {
343
+ if (prior.length === 0) {
344
+ return [
345
+ {
346
+ tool: queryTool,
347
+ input: {
348
+ toolUseIntent: "Survey table/column metadata",
349
+ sql: `SELECT table_name, column_name, data_type FROM information_schema.columns${whereSchema} ORDER BY table_name, ordinal_position LIMIT 1000`
350
+ }
351
+ }
352
+ ];
353
+ }
354
+ if (prior.length === 1) {
355
+ return distinctTables(prior, maxSampleTables).map((name) => {
356
+ const ref = opts.schemaLiteral ? `${quoteIdent(opts.schemaLiteral)}.${quoteIdent(name)}` : quoteIdent(name);
357
+ return {
358
+ tool: queryTool,
359
+ input: {
360
+ toolUseIntent: `Sample ${name}`,
361
+ sql: sampleSql(ref, sampleLimit)
362
+ }
363
+ };
364
+ });
365
+ }
366
+ return [];
367
+ };
368
+ }
369
+
268
370
  // ../connectors/src/setup-flow.ts
269
371
  async function runSetupFlow(flow, params, ctx, config) {
270
372
  const runtime = {
@@ -335,6 +437,58 @@ async function resolveSetupSelection(params) {
335
437
  return resolved.slice(0, limit);
336
438
  }
337
439
 
440
+ // ../connectors/src/introspect-concurrency.ts
441
+ async function mapWithConcurrency(items, limit, fn) {
442
+ const results = new Array(items.length);
443
+ let next = 0;
444
+ const workers = Array.from(
445
+ { length: Math.min(Math.max(limit, 1), items.length) },
446
+ async () => {
447
+ for (; ; ) {
448
+ const i = next++;
449
+ if (i >= items.length) return;
450
+ results[i] = await fn(items[i]);
451
+ }
452
+ }
453
+ );
454
+ await Promise.all(workers);
455
+ return results;
456
+ }
457
+
458
+ // ../connectors/src/introspect-profile.ts
459
+ var DEFAULT_MAX_TABLES = 200;
460
+ var DEFAULT_CONCURRENCY = 5;
461
+ function dq(ident) {
462
+ return `"${ident.replace(/"/g, '""')}"`;
463
+ }
464
+ var quoteAnsi = (schema, name) => [...schema.split("."), name].map(dq).join(".");
465
+ async function withRowCounts(ctx, schemaPromise, quoteRef, opts = {}) {
466
+ const schema = await schemaPromise;
467
+ if (!ctx.profile || !ctx.query) return schema;
468
+ return attachRowCounts(ctx.query, schema, quoteRef, opts);
469
+ }
470
+ async function attachRowCounts(run, schema, quoteRef, opts = {}) {
471
+ const targets = schema.tables.slice(0, opts.maxTables ?? DEFAULT_MAX_TABLES);
472
+ await mapWithConcurrency(
473
+ targets,
474
+ opts.concurrency ?? DEFAULT_CONCURRENCY,
475
+ async (table) => {
476
+ try {
477
+ const { rows } = await run(
478
+ `SELECT COUNT(*) AS row_count FROM ${quoteRef(table.schema, table.name)}`
479
+ );
480
+ const first = rows[0] ? Object.values(rows[0])[0] : void 0;
481
+ const rowCount = Number(first);
482
+ if (Number.isFinite(rowCount)) {
483
+ table.profile = { ...table.profile ?? {}, rowCount };
484
+ }
485
+ } catch {
486
+ }
487
+ }
488
+ );
489
+ return schema;
490
+ }
491
+
338
492
  // ../connectors/src/auth-types.ts
339
493
  var AUTH_TYPES = {
340
494
  OAUTH: "oauth",
@@ -360,6 +514,116 @@ function buildPositionalParams(sql, namedParams) {
360
514
  return { text, values };
361
515
  }
362
516
 
517
+ // ../connectors/src/introspect-information-schema.ts
518
+ function pickCI(row, key) {
519
+ if (key in row) return String(row[key] ?? "");
520
+ const lower = key.toLowerCase();
521
+ for (const k of Object.keys(row)) {
522
+ if (k.toLowerCase() === lower) return String(row[k] ?? "");
523
+ }
524
+ return "";
525
+ }
526
+ function assembleIntrospectedSchema(input) {
527
+ const { colRows, pkRows = [], fkRows = [] } = input;
528
+ const pkSet = /* @__PURE__ */ new Set();
529
+ for (const r of pkRows) {
530
+ pkSet.add(
531
+ `${pickCI(r, "table_schema")}.${pickCI(r, "table_name")}.${pickCI(r, "column_name")}`
532
+ );
533
+ }
534
+ const fkMap = /* @__PURE__ */ new Map();
535
+ for (const r of fkRows) {
536
+ fkMap.set(
537
+ `${pickCI(r, "table_schema")}.${pickCI(r, "table_name")}.${pickCI(r, "column_name")}`,
538
+ {
539
+ schema: pickCI(r, "foreign_table_schema"),
540
+ table: pickCI(r, "foreign_table_name"),
541
+ column: pickCI(r, "foreign_column_name")
542
+ }
543
+ );
544
+ }
545
+ const tableMap = /* @__PURE__ */ new Map();
546
+ for (const r of colRows) {
547
+ const schema = pickCI(r, "table_schema");
548
+ const name = pickCI(r, "table_name");
549
+ const key = `${schema}.${name}`;
550
+ let table = tableMap.get(key);
551
+ if (!table) {
552
+ table = {
553
+ schema,
554
+ name,
555
+ type: pickCI(r, "table_type").toUpperCase() === "VIEW" ? "view" : "table",
556
+ columns: []
557
+ };
558
+ tableMap.set(key, table);
559
+ }
560
+ const colKey = `${schema}.${name}.${pickCI(r, "column_name")}`;
561
+ const column = {
562
+ name: pickCI(r, "column_name"),
563
+ dataType: pickCI(r, "data_type"),
564
+ nullable: pickCI(r, "is_nullable").toUpperCase() === "YES"
565
+ };
566
+ if (pkSet.has(colKey)) column.isPrimaryKey = true;
567
+ const fk = fkMap.get(colKey);
568
+ if (fk) column.foreignKey = fk;
569
+ table.columns.push(column);
570
+ }
571
+ return { tables: [...tableMap.values()] };
572
+ }
573
+
574
+ // ../connectors/src/connectors/postgresql/introspect.ts
575
+ async function introspectPostgresql(run) {
576
+ const SCHEMA_FILTER = `c.table_schema NOT IN ('pg_catalog', 'information_schema') AND c.table_schema NOT LIKE 'pg\\_%'`;
577
+ const [{ rows: colRows }, { rows: pkRows }, { rows: fkRows }] = await Promise.all([
578
+ // 1) 全カラム(型 / nullable)+ テーブル種別(BASE TABLE / VIEW)
579
+ run(
580
+ `SELECT c.table_schema, c.table_name, c.column_name, c.data_type,
581
+ c.is_nullable, c.ordinal_position, t.table_type
582
+ FROM information_schema.columns c
583
+ JOIN information_schema.tables t
584
+ ON t.table_schema = c.table_schema AND t.table_name = c.table_name
585
+ WHERE ${SCHEMA_FILTER}
586
+ ORDER BY c.table_schema, c.table_name, c.ordinal_position`
587
+ ),
588
+ // 2) 主キー(schema.table.column)
589
+ run(
590
+ `SELECT tc.table_schema, tc.table_name, kcu.column_name
591
+ FROM information_schema.table_constraints tc
592
+ JOIN information_schema.key_column_usage kcu
593
+ ON kcu.constraint_name = tc.constraint_name
594
+ AND kcu.table_schema = tc.table_schema
595
+ WHERE tc.constraint_type = 'PRIMARY KEY'
596
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
597
+ AND tc.table_schema NOT LIKE 'pg\\_%'`
598
+ ),
599
+ // 3) 外部キー(参照元 → 参照先)。
600
+ // 複合 FK を正しく対応付けるため、`constraint_column_usage`(順序非保証 + constraint_name のみの
601
+ // join で複合キーが直積爆発する)を使わず、`referential_constraints` 経由で参照先 unique 制約へ辿り、
602
+ // FK 列の `position_in_unique_constraint` を参照先列の `ordinal_position` に突き合わせて 1:1 に組む。
603
+ run(
604
+ `SELECT tc.table_schema, tc.table_name, kcu.column_name,
605
+ fkcu.table_schema AS foreign_table_schema,
606
+ fkcu.table_name AS foreign_table_name,
607
+ fkcu.column_name AS foreign_column_name
608
+ FROM information_schema.table_constraints tc
609
+ JOIN information_schema.key_column_usage kcu
610
+ ON kcu.constraint_name = tc.constraint_name
611
+ AND kcu.constraint_schema = tc.constraint_schema
612
+ JOIN information_schema.referential_constraints rc
613
+ ON rc.constraint_name = tc.constraint_name
614
+ AND rc.constraint_schema = tc.constraint_schema
615
+ JOIN information_schema.key_column_usage fkcu
616
+ ON fkcu.constraint_name = rc.unique_constraint_name
617
+ AND fkcu.constraint_schema = rc.unique_constraint_schema
618
+ AND fkcu.ordinal_position = kcu.position_in_unique_constraint
619
+ WHERE tc.constraint_type = 'FOREIGN KEY'
620
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
621
+ AND tc.table_schema NOT LIKE 'pg\\_%'`
622
+ )
623
+ ]);
624
+ return assembleIntrospectedSchema({ colRows, pkRows, fkRows });
625
+ }
626
+
363
627
  // ../connectors/src/connectors/supabase/setup.ts
364
628
  var supabaseOnboarding = new ConnectorOnboarding({
365
629
  dataOverviewInstructions: {
@@ -469,6 +733,21 @@ var supabaseSetupFlow = {
469
733
  fetchAll: () => fetchTableNames(rt.params, schema),
470
734
  limit: SUPABASE_SETUP_MAX_TABLES
471
735
  });
736
+ let rowCounts = /* @__PURE__ */ new Map();
737
+ try {
738
+ const countRows = await runQuery(
739
+ rt.params,
740
+ `SELECT relname, n_live_tup FROM pg_stat_user_tables WHERE schemaname = $1`,
741
+ [schema]
742
+ );
743
+ rowCounts = new Map(
744
+ countRows.map((r) => [
745
+ String(r["relname"] ?? ""),
746
+ Number(r["n_live_tup"] ?? 0)
747
+ ])
748
+ );
749
+ } catch {
750
+ }
472
751
  const sections = [
473
752
  "## Supabase",
474
753
  "",
@@ -484,7 +763,9 @@ var supabaseSetupFlow = {
484
763
  ORDER BY ordinal_position`,
485
764
  [schema, table]
486
765
  );
487
- sections.push(`#### Table: ${table}`, "");
766
+ const rowCount = rowCounts.get(table);
767
+ const heading = typeof rowCount === "number" ? `#### Table: ${table} (~${rowCount.toLocaleString()} rows)` : `#### Table: ${table}`;
768
+ sections.push(heading, "");
488
769
  sections.push("| Column | Type | Nullable | Default |");
489
770
  sections.push("|--------|------|----------|---------|");
490
771
  for (const c of cols) {
@@ -520,6 +801,9 @@ var outputSchema = z.discriminatedUnion("success", [
520
801
  z.object({
521
802
  success: z.literal(true),
522
803
  rowCount: z.number(),
804
+ rowsAffected: z.number().optional().describe(
805
+ "Affected rows for INSERT/UPDATE/DELETE; undefined for SELECT"
806
+ ),
523
807
  truncated: z.boolean(),
524
808
  rows: z.array(z.record(z.string(), z.unknown()))
525
809
  }),
@@ -533,7 +817,8 @@ var executeQueryTool = new ConnectorTool({
533
817
  description: `Execute SQL against a Supabase Postgres database. Returns up to ${MAX_ROWS} rows.
534
818
  Use for: schema exploration (information_schema), data sampling, and analytical queries against Supabase tables.
535
819
  User tables live in the \`public\` schema by default; Supabase-managed metadata lives in \`auth\` and \`storage\`.
536
- Avoid loading large amounts of data; always include LIMIT in queries.`,
820
+ Avoid loading large amounts of data; always include LIMIT in queries.
821
+ For INSERT/UPDATE/DELETE, \`rowCount\` is 0 and \`rows\` is empty (rowCount counts returned rows, not affected rows) \u2014 this is normal, not a failure; check \`rowsAffected\` for the number of affected rows. Use RETURNING to get written rows back in \`rows\`.`,
537
822
  inputSchema,
538
823
  outputSchema,
539
824
  async execute({ connectionId, sql }, connections) {
@@ -561,9 +846,13 @@ Avoid loading large amounts of data; always include LIMIT in queries.`,
561
846
  const result = await pool.query(sql);
562
847
  const rows = result.rows;
563
848
  const truncated = rows.length > MAX_ROWS;
849
+ const isDml = ["INSERT", "UPDATE", "DELETE", "MERGE"].includes(
850
+ result.command
851
+ );
564
852
  return {
565
853
  success: true,
566
854
  rowCount: Math.min(rows.length, MAX_ROWS),
855
+ rowsAffected: isDml ? result.rowCount ?? 0 : void 0,
567
856
  truncated,
568
857
  rows: rows.slice(0, MAX_ROWS)
569
858
  };
@@ -595,7 +884,7 @@ var supabaseConnector = new ConnectorPlugin({
595
884
  systemPrompt: {
596
885
  en: `### Tools
597
886
 
598
- - \`supabase_executeQuery\`: Executes a PostgreSQL query against the Supabase project's database and returns rows. Use this for schema exploration via \`information_schema.tables\`/\`columns\` and for sampling data. See the SQL Reference below for syntax notes and Supabase-specific schemas.
887
+ - \`connector_supabase_executeQuery\`: Executes a PostgreSQL query against the Supabase project's database and returns rows. Use this for schema exploration via \`information_schema.tables\`/\`columns\` and for sampling data. See the SQL Reference below for syntax notes and Supabase-specific schemas.
599
888
 
600
889
  ### Business Logic
601
890
 
@@ -612,7 +901,7 @@ The business logic type for this connector is "sql".
612
901
  - Always include LIMIT in queries`,
613
902
  ja: `### \u30C4\u30FC\u30EB
614
903
 
615
- - \`supabase_executeQuery\`: Supabase\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E Postgres \u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306B\u5BFE\u3057\u3066\u30AF\u30A8\u30EA\u3092\u5B9F\u884C\u3057\u3001\u884C\u30C7\u30FC\u30BF\u3092\u8FD4\u3057\u307E\u3059\u3002\`information_schema.tables\`/\`columns\` \u3092\u4F7F\u3063\u305F\u30B9\u30AD\u30FC\u30DE\u63A2\u7D22\u3084\u30C7\u30FC\u30BF\u306E\u30B5\u30F3\u30D7\u30EA\u30F3\u30B0\u306B\u4F7F\u3044\u307E\u3059\u3002\u69CB\u6587\u306E\u6CE8\u610F\u70B9\u3068 Supabase \u56FA\u6709\u306E\u30B9\u30AD\u30FC\u30DE\u306F\u4E0B\u90E8\u306E\u300CSQL \u30EA\u30D5\u30A1\u30EC\u30F3\u30B9\u300D\u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
904
+ - \`connector_supabase_executeQuery\`: Supabase\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E Postgres \u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306B\u5BFE\u3057\u3066\u30AF\u30A8\u30EA\u3092\u5B9F\u884C\u3057\u3001\u884C\u30C7\u30FC\u30BF\u3092\u8FD4\u3057\u307E\u3059\u3002\`information_schema.tables\`/\`columns\` \u3092\u4F7F\u3063\u305F\u30B9\u30AD\u30FC\u30DE\u63A2\u7D22\u3084\u30C7\u30FC\u30BF\u306E\u30B5\u30F3\u30D7\u30EA\u30F3\u30B0\u306B\u4F7F\u3044\u307E\u3059\u3002\u69CB\u6587\u306E\u6CE8\u610F\u70B9\u3068 Supabase \u56FA\u6709\u306E\u30B9\u30AD\u30FC\u30DE\u306F\u4E0B\u90E8\u306E\u300CSQL \u30EA\u30D5\u30A1\u30EC\u30F3\u30B9\u300D\u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
616
905
 
617
906
  ### Business Logic
618
907
 
@@ -629,6 +918,11 @@ The business logic type for this connector is "sql".
629
918
  - \u30AF\u30A8\u30EA\u306B\u306F\u5FC5\u305A LIMIT \u3092\u542B\u3081\u3066\u304F\u3060\u3055\u3044`
630
919
  },
631
920
  tools,
921
+ // 決定論的な立ち上げ survey(ルールベース prewarm)。Supabase は Postgres で公開スキーマは `public`。
922
+ surveyProbes: makeStandardSqlSurveyProbes({ schemaLiteral: "public" }),
923
+ // Supabase は PostgreSQL なので postgresql の introspect をそのまま流用する
924
+ // (exploreData / DataContextIndex の対象に含めるため)。
925
+ introspect: (ctx) => withRowCounts(ctx, introspectPostgresql(ctx.query), quoteAnsi),
632
926
  setup: (params, ctx, config) => runSetupFlow(supabaseSetupFlow, params, ctx, config),
633
927
  async checkConnection(params, _config) {
634
928
  const { Pool } = await import("pg");
@@ -1,6 +1,11 @@
1
1
  var __getOwnPropNames = Object.getOwnPropertyNames;
2
- var __esm = (fn, res) => function __init() {
3
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
2
+ var __esm = (fn, res, err) => function __init() {
3
+ if (err) throw err[0];
4
+ try {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ } catch (e) {
7
+ throw err = [e], e;
8
+ }
4
9
  };
5
10
 
6
11
  // ../connectors/src/parameter-definition.ts
@@ -266,6 +271,15 @@ var ConnectorPlugin = class _ConnectorPlugin {
266
271
  * `runSetupFlow` from `setup-flow.ts`.
267
272
  */
268
273
  setup;
274
+ /**
275
+ * 統一 introspection。SQL DB の `information_schema` / `SHOW` / `DESCRIBE` や SaaS の
276
+ * メタデータ API といった取得経路の差を吸収し、tables / columns / PK / FK を共通型
277
+ * {@link IntrospectedSchema} へ正規化して返す。取得経路は {@link IntrospectContext} で
278
+ * 抽象化され、SQL DB は `ctx.query`、SaaS は `ctx.proxyFetch` + `ctx.params` を使う。
279
+ * `ctx.profile === true` のときは L2 プロファイル(行数・NULL 率等)も best-effort で埋める。
280
+ * coding-agent の `DataContextIndex` がデータ地図を構築する際に使う。
281
+ */
282
+ introspect;
269
283
  /**
270
284
  * Opt-out of the default "verify before save" behavior on connection
271
285
  * creation. The backend invokes `checkConnection` synchronously while
@@ -280,6 +294,14 @@ var ConnectorPlugin = class _ConnectorPlugin {
280
294
  * the default verify-on-create behavior without opt-in.
281
295
  */
282
296
  skipConnectionCheckOnCreate;
297
+ /**
298
+ * 決定論的な「立ち上げ survey」レシピ(ルールベース prewarm 基盤)。実装した connector は、LLM を使わずに
299
+ * read-only で自分を survey する probe 列(テーブル/列メタデータ → 小サンプル)を宣言する。coding-agent の
300
+ * recon driver がこれを読み、connector ツールの `execute` を直接呼んで結果を seed にする。未実装の connector は
301
+ * driver 側で safely skip される(その connection は listConnections のみ seed)。標準 `information_schema` を
302
+ * 持つ SQL connector は `makeStandardSqlSurveyProbes` を使える。**probe は必ず read-only**。
303
+ */
304
+ surveyProbes;
283
305
  constructor(config) {
284
306
  this.slug = config.slug;
285
307
  this.authType = config.authType;
@@ -297,7 +319,9 @@ var ConnectorPlugin = class _ConnectorPlugin {
297
319
  this.query = config.query;
298
320
  this.checkConnection = config.checkConnection;
299
321
  this.setup = config.setup;
322
+ this.introspect = config.introspect;
300
323
  this.skipConnectionCheckOnCreate = config.skipConnectionCheckOnCreate;
324
+ this.surveyProbes = config.surveyProbes;
301
325
  }
302
326
  get connectorKey() {
303
327
  return _ConnectorPlugin.deriveKey(this.slug, this.authType);
@@ -305,7 +329,7 @@ var ConnectorPlugin = class _ConnectorPlugin {
305
329
  /**
306
330
  * Create tools for connections that belong to this connector.
307
331
  * Filters connections by connectorKey internally.
308
- * Returns tools keyed as `${connectorKey}_${toolName}`.
332
+ * Returns tools keyed as `connector_${connectorKey}_${toolName}`.
309
333
  */
310
334
  createTools(connections, config, opts) {
311
335
  const myConnections = connections.filter(
@@ -315,7 +339,7 @@ var ConnectorPlugin = class _ConnectorPlugin {
315
339
  for (const t of Object.values(this.tools)) {
316
340
  const tool = t.createTool(myConnections, config);
317
341
  const originalToModelOutput = tool.toModelOutput;
318
- result[`${this.connectorKey}_${t.name}`] = {
342
+ result[`connector_${this.connectorKey}_${t.name}`] = {
319
343
  ...tool,
320
344
  toModelOutput: async (options) => {
321
345
  if (!originalToModelOutput) {
@@ -335,25 +359,25 @@ var ConnectorPlugin = class _ConnectorPlugin {
335
359
  if (authType) return `${slug}-${authType}`;
336
360
  const LEGACY_NULL_AUTH_TYPE_MAP = {
337
361
  // user-password
338
- "postgresql": "user-password",
339
- "mysql": "user-password",
340
- "clickhouse": "user-password",
341
- "kintone": "user-password",
362
+ postgresql: "user-password",
363
+ mysql: "user-password",
364
+ clickhouse: "user-password",
365
+ kintone: "user-password",
342
366
  "squadbase-db": "user-password",
343
367
  // service-account
344
- "snowflake": "service-account",
345
- "bigquery": "service-account",
368
+ snowflake: "service-account",
369
+ bigquery: "service-account",
346
370
  "google-analytics": "service-account",
347
371
  "google-calendar": "service-account",
348
372
  "aws-athena": "service-account",
349
- "redshift": "service-account",
373
+ redshift: "service-account",
350
374
  // api-key
351
- "databricks": "api-key",
352
- "dbt": "api-key",
353
- "airtable": "api-key",
354
- "openai": "api-key",
355
- "gemini": "api-key",
356
- "anthropic": "api-key",
375
+ databricks: "api-key",
376
+ dbt: "api-key",
377
+ airtable: "api-key",
378
+ openai: "api-key",
379
+ gemini: "api-key",
380
+ anthropic: "api-key",
357
381
  "wix-store": "api-key"
358
382
  };
359
383
  const fallbackAuthType = LEGACY_NULL_AUTH_TYPE_MAP[slug];
@@ -447,7 +471,7 @@ var tableauOnboarding = new ConnectorOnboarding({
447
471
  connectionSetupInstructions: {
448
472
  en: `Follow these steps to verify the Tableau PAT connection.
449
473
 
450
- 1. Call \`tableau_request\` with \`method: "GET"\` and \`path: "/sites/{siteId}/projects?pageSize=5"\` to list a few projects on the signed-in site
474
+ 1. Call \`connector_tableau_request\` with \`method: "GET"\` and \`path: "/sites/{siteId}/projects?pageSize=5"\` to list a few projects on the signed-in site
451
475
  2. If the call fails with HTTP 401, ask the user to confirm the PAT name/secret and that the Site Content URL matches the site where the PAT was issued (Tableau Cloud sites are case-sensitive)
452
476
 
453
477
  #### Constraints
@@ -455,7 +479,7 @@ var tableauOnboarding = new ConnectorOnboarding({
455
479
  - The literal placeholder \`{siteId}\` in the path is automatically replaced with the session's site ID \u2014 do NOT hardcode the ID`,
456
480
  ja: `Tableau PAT \u30B3\u30CD\u30AF\u30B7\u30E7\u30F3\u306E\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u306F\u4EE5\u4E0B\u306E\u624B\u9806\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044\u3002
457
481
 
458
- 1. \`tableau_request\` \u3092 \`method: "GET"\`\u3001\`path: "/sites/{siteId}/projects?pageSize=5"\` \u3067\u547C\u3073\u51FA\u3057\u3001\u30B5\u30A4\u30F3\u30A4\u30F3\u5148\u30B5\u30A4\u30C8\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u3044\u304F\u3064\u304B\u53D6\u5F97\u3059\u308B
482
+ 1. \`connector_tableau_request\` \u3092 \`method: "GET"\`\u3001\`path: "/sites/{siteId}/projects?pageSize=5"\` \u3067\u547C\u3073\u51FA\u3057\u3001\u30B5\u30A4\u30F3\u30A4\u30F3\u5148\u30B5\u30A4\u30C8\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u3044\u304F\u3064\u304B\u53D6\u5F97\u3059\u308B
459
483
  2. HTTP 401 \u3067\u5931\u6557\u3057\u305F\u5834\u5408\u306F\u3001PAT \u540D/\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8\u304A\u3088\u3073 Site Content URL \u304C PAT \u767A\u884C\u30B5\u30A4\u30C8\u3068\u4E00\u81F4\u3057\u3066\u3044\u308B\u304B\u3092\u30E6\u30FC\u30B6\u30FC\u306B\u78BA\u8A8D\u3059\u308B\u3088\u3046\u4F1D\u3048\u308B\uFF08Tableau Cloud \u306E\u30B5\u30A4\u30C8\u540D\u306F\u5927\u6587\u5B57\u5C0F\u6587\u5B57\u3092\u533A\u5225\uFF09
460
484
 
461
485
  #### \u5236\u7D04
@@ -463,14 +487,14 @@ var tableauOnboarding = new ConnectorOnboarding({
463
487
  - \u30D1\u30B9\u5185\u306E\u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u30FC \`{siteId}\` \u306F\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u30B5\u30A4\u30C8 ID \u3067\u81EA\u52D5\u7F6E\u63DB\u3055\u308C\u307E\u3059 \u2014 ID \u3092\u30CF\u30FC\u30C9\u30B3\u30FC\u30C9\u3057\u306A\u3044\u3053\u3068`
464
488
  },
465
489
  dataOverviewInstructions: {
466
- en: `1. Call tableau_request with GET /sites/{siteId}/projects to list projects
467
- 2. Call tableau_request with GET /sites/{siteId}/workbooks?pageSize=20 to list workbooks
468
- 3. For a target workbook, call tableau_request with GET /sites/{siteId}/workbooks/{workbookId}/views to list its views
469
- 4. Call tableau_request with GET /sites/{siteId}/datasources?pageSize=20 to list published data sources`,
470
- ja: `1. tableau_request \u3067 GET /sites/{siteId}/projects \u3092\u547C\u3073\u51FA\u3057\u3001\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u3092\u53D6\u5F97
471
- 2. tableau_request \u3067 GET /sites/{siteId}/workbooks?pageSize=20 \u3092\u547C\u3073\u51FA\u3057\u3001\u30EF\u30FC\u30AF\u30D6\u30C3\u30AF\u4E00\u89A7\u3092\u53D6\u5F97
472
- 3. \u5BFE\u8C61\u30EF\u30FC\u30AF\u30D6\u30C3\u30AF\u306B\u3064\u3044\u3066 tableau_request \u3067 GET /sites/{siteId}/workbooks/{workbookId}/views \u3092\u547C\u3073\u51FA\u3057\u3001\u30D3\u30E5\u30FC\u4E00\u89A7\u3092\u53D6\u5F97
473
- 4. tableau_request \u3067 GET /sites/{siteId}/datasources?pageSize=20 \u3092\u547C\u3073\u51FA\u3057\u3001\u516C\u958B\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u4E00\u89A7\u3092\u53D6\u5F97`
490
+ en: `1. Call connector_tableau_request with GET /sites/{siteId}/projects to list projects
491
+ 2. Call connector_tableau_request with GET /sites/{siteId}/workbooks?pageSize=20 to list workbooks
492
+ 3. For a target workbook, call connector_tableau_request with GET /sites/{siteId}/workbooks/{workbookId}/views to list its views
493
+ 4. Call connector_tableau_request with GET /sites/{siteId}/datasources?pageSize=20 to list published data sources`,
494
+ ja: `1. connector_tableau_request \u3067 GET /sites/{siteId}/projects \u3092\u547C\u3073\u51FA\u3057\u3001\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u3092\u53D6\u5F97
495
+ 2. connector_tableau_request \u3067 GET /sites/{siteId}/workbooks?pageSize=20 \u3092\u547C\u3073\u51FA\u3057\u3001\u30EF\u30FC\u30AF\u30D6\u30C3\u30AF\u4E00\u89A7\u3092\u53D6\u5F97
496
+ 3. \u5BFE\u8C61\u30EF\u30FC\u30AF\u30D6\u30C3\u30AF\u306B\u3064\u3044\u3066 connector_tableau_request \u3067 GET /sites/{siteId}/workbooks/{workbookId}/views \u3092\u547C\u3073\u51FA\u3057\u3001\u30D3\u30E5\u30FC\u4E00\u89A7\u3092\u53D6\u5F97
497
+ 4. connector_tableau_request \u3067 GET /sites/{siteId}/datasources?pageSize=20 \u3092\u547C\u3073\u51FA\u3057\u3001\u516C\u958B\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u4E00\u89A7\u3092\u53D6\u5F97`
474
498
  }
475
499
  });
476
500
 
@@ -917,13 +941,13 @@ var tableauConnector = new ConnectorPlugin({
917
941
  description: "Connect to Tableau Cloud or Tableau Server via a Personal Access Token (PAT). Use it to enumerate projects, workbooks, views, and data sources, and to pull view data.",
918
942
  iconUrl: "https://images.ctfassets.net/9ncizv60xc5y/3ejrZvfw7zCDa3FPbUNQx9/d810e117b3a86c45dd96205453bf67a0/tableau-icon.svg",
919
943
  parameters,
920
- releaseFlag: { dev1: true, dev2: false, prod: false },
944
+ releaseFlag: { dev1: true, dev2: true, prod: true },
921
945
  categories: ["bi"],
922
946
  onboarding: tableauOnboarding,
923
947
  systemPrompt: {
924
948
  en: `### Tools
925
949
 
926
- - \`tableau_request\`: The only way to call the Tableau REST API. Use it for projects, workbooks, views, data sources, users, and permissions. The \`X-Tableau-Auth\` token is issued and managed centrally by the Squadbase backend so concurrent processes (chat & deployed app) share a single PAT sign-in. Use the literal \`{siteId}\` placeholder in the path \u2014 it is replaced with the session's site ID automatically.
950
+ - \`connector_tableau_request\`: The only way to call the Tableau REST API. Use it for projects, workbooks, views, data sources, users, and permissions. The \`X-Tableau-Auth\` token is issued and managed centrally by the Squadbase backend so concurrent processes (chat & deployed app) share a single PAT sign-in. Use the literal \`{siteId}\` placeholder in the path \u2014 it is replaced with the session's site ID automatically.
927
951
 
928
952
  ### Business Logic
929
953
 
@@ -980,7 +1004,7 @@ export default async function handler(c: Context) {
980
1004
  - Combine: \`filter=ownerName:eq:alice,tags:in:[finance]\``,
981
1005
  ja: `### \u30C4\u30FC\u30EB
982
1006
 
983
- - \`tableau_request\`: Tableau REST API \u3092\u547C\u3073\u51FA\u3059\u552F\u4E00\u306E\u624B\u6BB5\u3067\u3059\u3002\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3001\u30EF\u30FC\u30AF\u30D6\u30C3\u30AF\u3001\u30D3\u30E5\u30FC\u3001\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u3001\u30E6\u30FC\u30B6\u30FC\u3001\u6A29\u9650\u306A\u3069\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002\`X-Tableau-Auth\` \u30C8\u30FC\u30AF\u30F3\u306F Squadbase \u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u3067\u4E00\u5143\u7BA1\u7406\u3055\u308C\u3001\u8907\u6570\u30D7\u30ED\u30BB\u30B9 (\u30C1\u30E3\u30C3\u30C8\u30FB\u30C7\u30D7\u30ED\u30A4\u6E08\u307F\u30A2\u30D7\u30EA) \u3067\u540C\u3058 PAT \u30B5\u30A4\u30F3\u30A4\u30F3\u3092\u5171\u6709\u3057\u307E\u3059\u3002\u30D1\u30B9\u5185\u306B\u306F \`{siteId}\` \u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u30FC\u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044 \u2014 \u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u30B5\u30A4\u30C8 ID \u3067\u81EA\u52D5\u7F6E\u63DB\u3055\u308C\u307E\u3059\u3002
1007
+ - \`connector_tableau_request\`: Tableau REST API \u3092\u547C\u3073\u51FA\u3059\u552F\u4E00\u306E\u624B\u6BB5\u3067\u3059\u3002\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3001\u30EF\u30FC\u30AF\u30D6\u30C3\u30AF\u3001\u30D3\u30E5\u30FC\u3001\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u3001\u30E6\u30FC\u30B6\u30FC\u3001\u6A29\u9650\u306A\u3069\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002\`X-Tableau-Auth\` \u30C8\u30FC\u30AF\u30F3\u306F Squadbase \u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u3067\u4E00\u5143\u7BA1\u7406\u3055\u308C\u3001\u8907\u6570\u30D7\u30ED\u30BB\u30B9 (\u30C1\u30E3\u30C3\u30C8\u30FB\u30C7\u30D7\u30ED\u30A4\u6E08\u307F\u30A2\u30D7\u30EA) \u3067\u540C\u3058 PAT \u30B5\u30A4\u30F3\u30A4\u30F3\u3092\u5171\u6709\u3057\u307E\u3059\u3002\u30D1\u30B9\u5185\u306B\u306F \`{siteId}\` \u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u30FC\u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044 \u2014 \u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u30B5\u30A4\u30C8 ID \u3067\u81EA\u52D5\u7F6E\u63DB\u3055\u308C\u307E\u3059\u3002
984
1008
 
985
1009
  ### Business Logic
986
1010