@tailor-platform/sdk 2.28.0 → 2.29.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/CHANGELOG.md +61 -0
- package/README.md +0 -8
- package/dist/{application-CvmKh5WT.mjs → application-dalGHdrI.mjs} +1 -1
- package/dist/{application-CgJLyJaF.mjs → application-rWYDAfNU.mjs} +29 -29
- package/dist/application-rWYDAfNU.mjs.map +1 -0
- package/dist/cli/cache/bundle-cache.d.mts +1 -0
- package/dist/cli/lib.mjs +1 -1
- package/dist/cli/lib.mjs.map +1 -1
- package/dist/cli/main.d.mts +5 -0
- package/dist/cli/main.mjs +46 -46
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/ts-hook.mjs +15 -11
- package/dist/completion/zsh-worker.zsh +59 -2
- package/dist/configure/config/types.d.mts +57 -0
- package/dist/{register-ts-hook-yxshv0LV.mjs → register-ts-hook-CkfKqhJT.mjs} +78 -77
- package/dist/register-ts-hook-CkfKqhJT.mjs.map +1 -0
- package/dist/seed/index.d.mts +1 -1
- package/dist/seed/index.mjs +1 -1
- package/dist/seed/index.mjs.map +1 -1
- package/dist/seed-C3P_T_Eh.mjs.map +1 -1
- package/dist/vitest/mocks/file.d.mts +1 -1
- package/docs/cli/application.md +8 -3
- package/docs/cli/tailordb.md +34 -0
- package/docs/cli-reference.md +11 -0
- package/docs/configuration.md +53 -2
- package/docs/github-actions.md +90 -7
- package/docs/plugin/custom.md +1 -1
- package/docs/services/resolver.md +1 -58
- package/docs/services/tailordb-migration.md +12 -3
- package/docs/services/workflow.md +2 -2
- package/docs/testing.md +2 -2
- package/package.json +16 -16
- package/dist/application-CgJLyJaF.mjs.map +0 -1
- package/dist/register-ts-hook-yxshv0LV.mjs.map +0 -1
package/dist/seed/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ForeignKeyDefinition, IndexDefinition, defineSchema } from "
|
|
1
|
+
import { ForeignKeyDefinition, IndexDefinition, defineSchema } from "lines-db";
|
|
2
2
|
//#region src/seed/index.d.ts
|
|
3
3
|
type ValidateSeedDataOptions = {
|
|
4
4
|
/** Resolved absolute path to a data directory or a .jsonl file */
|
package/dist/seed/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{basename as e,dirname as t,join as n,normalize as r}from"pathe";import{readFile as i,readdir as a,stat as o}from"node:fs/promises";import{ErrorFormatter as s,LinesDB as c,defineSchema as l,fillFields as u,findSchemaFile as d,isSchemaFile as f,unwrap as p}from"
|
|
1
|
+
import{basename as e,dirname as t,join as n,normalize as r}from"pathe";import{readFile as i,readdir as a,stat as o}from"node:fs/promises";import{ErrorFormatter as s,LinesDB as c,defineSchema as l,fillFields as u,findSchemaFile as d,isSchemaFile as f,unwrap as p}from"lines-db";function physicalLineOfRecord(e,t){let n=e.split(`
|
|
2
2
|
`),r=t;for(let[e,t]of n.entries())if(t.trim().length!==0){if(r===0)return e+1;--r}}async function firstErrorLocation(e){let t=e[0];if(t)try{let e=await i(t.file,`utf-8`);return{file:t.file,line:physicalLineOfRecord(e,t.rowIndex)}}catch{return{file:t.file}}}const m=[`id`];async function hasSchemaFiles(e){try{return(await a(e)).some(e=>f(e))}catch{return!1}}async function datasetHint(n,r){let i=t(n);return i===n||await hasSchemaFiles(n)||!await hasSchemaFiles(i)?[]:[`${n} has no schema files of its own. To ${r} it as a dataset with the schema files in ${i}, run \`tailor seed ${r} --dataset ${e(n)}\`.`]}async function resolveSeedDataTarget(n){let r=await o(n);if(r.isDirectory())return{dataDir:n};if(r.isFile()&&n.endsWith(`.jsonl`))return{dataDir:t(n),tableName:e(n,`.jsonl`)};throw Error(`Invalid path: ${n}. Must be a directory or .jsonl file.`)}async function resolveValidationConfig(e){let{path:t,datasetDirs:n=[],excludeBase:r=!1}=e;if(n.length===0){if(r)throw Error(`Leaving the base data out needs at least one dataset to validate.`);let{dataDir:e,tableName:n}=await resolveSeedDataTarget(t);return{dataDir:e,tableName:n,warnings:await datasetHint(e,`validate`)}}if(!(await o(t)).isDirectory())throw Error(`Invalid path: ${t}. Datasets are validated against the schema files of a data directory, so the path must be a directory.`);let i=r?[...n]:[t,...n];return{dataDir:i.length===1?i[0]:i,schemaDir:t,warnings:await findFilesWithoutSchema(n,t)}}async function findFilesWithoutSchema(e,t){let r=[];for(let i of e)for(let e of await listSeedTables(i))await d(t,e)||r.push(`${n(i,`${e}.jsonl`)} has no schema file in ${t}, so its rows are not checked`);return r}const h=/^Foreign key reference to non-existent table '(.+)' in table '(.+)'$/;async function listSeedTables(t){return(await a(t)).filter(e=>e.endsWith(`.jsonl`)).map(t=>e(t,`.jsonl`)).toSorted()}function formatWarnings(e){return e.length===0?[]:[...e.map(e=>`⚠ ${e}`),``]}function isJsonlParseError(e){return e.name===`JsonlParseError`}function formatValidationErrors(e,t){let n=new s({verbose:t}),r=[],i=/* @__PURE__ */ new Map;for(let t of e){let e=i.get(t.file)||[];e.push(t),i.set(t.file,e)}for(let[e,t]of i){r.push(n.formatErrorHeader(t.length,e)),r.push(``);let i=t.filter(e=>e.type!==`foreignKey`||!e.foreignKeyError),a=t.filter(e=>e.type===`foreignKey`&&e.foreignKeyError);i.length>0&&r.push(n.formatValidationErrors(i.map(e=>({file:e.file,rowIndex:e.rowIndex,issues:e.issues}))));for(let e of a)e.foreignKeyError&&r.push(n.formatForeignKeyError({file:e.file,rowIndex:e.rowIndex,column:e.foreignKeyError.column,value:e.foreignKeyError.value,referencedTable:e.foreignKeyError.referencedTable,referencedColumn:e.foreignKeyError.referencedColumn}));r.push(``)}return r.join(`
|
|
3
3
|
`)}async function validateSeedData(e){let{verbose:t=!1,excludeBase:n=!1}=e,{tableName:r,warnings:i,...a}=await resolveValidationConfig(e),o=c.create(a),l;try{l=await o.initialize({tableName:r,detailedValidate:!0})}finally{p(await o.close())}if(!l.ok&&isJsonlParseError(l.error)){let{file:e,line:n,message:r}=l.error;return{valid:!1,output:``,error:[new s({verbose:t}).formatErrorHeader(1,e),``,` Line ${n}: ${r}`,``].join(`
|
|
4
4
|
`),location:{file:e,line:n}}}if(n&&!l.ok){let e=h.exec(l.error.message);if(e){let[,t,n]=e;throw Error(`${n} has a foreign key to ${t}, but none of the selected datasets has ${t}.jsonl. Add the file to one of them, even an empty one, or validate them on top of the base seed data.`,{cause:l.error})}}let u=p(l),d=formatWarnings([...i,...u.warnings]);return u.valid?(d.push(`✓ All records are valid`),{valid:!0,output:d.join(`
|
package/dist/seed/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["lines","content","remaining","recordIndex","index","line","first","errors","readFile","DEFAULT_FILL_FIELDS","readdir","dir","isSchemaFile","entry","parent","dirname","dataDir","command","basename","stats","stat","resolvedPath","options","datasetDirs","excludeBase","dataDirs","warnings","dirs","table","findSchemaFile","schemaDir","join","MISSING_REFERENCED_TABLE","warning","error","formatter","ErrorFormatter","errorLines","errorsByFile","fileErrors","file","validationErrors","foreignKeyErrors","fkError","config","db","LinesDB","initialized","unwrap","message","missingTable","referencedTable","result","outputLines","verbose","fields","tables","tableName","fillFields","hook","schemaModule","schemaPath","filled","normalize","unreadableLines","tablesWithoutSchema","unproducedFields","tableFields","count","defineSchema"],"sources":["../../src/seed/record-lines.ts","../../src/seed/index.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { ValidationErrorDetail } from \"@toiroakr/lines-db\";\n\n/**\n * Resolve the 1-based physical line a JSONL record occupies.\n *\n * The seed reader drops blank and whitespace-only lines before indexing its\n * records, so a record's index is not its line number once a file contains\n * one. Reported locations have to point at the line a reader would open.\n * @param content - Raw JSONL file contents\n * @param recordIndex - 0-based index among the file's non-blank lines\n * @returns 1-based physical line, or undefined when the file holds no such record\n */\nexport function physicalLineOfRecord(content: string, recordIndex: number): number | undefined {\n const lines = content.split(\"\\n\");\n let remaining = recordIndex;\n for (const [index, line] of lines.entries()) {\n if (line.trim().length === 0) continue;\n if (remaining === 0) return index + 1;\n remaining -= 1;\n }\n return undefined;\n}\n\n/**\n * Locate the first reported error in its own file.\n *\n * Errors are reported per record, so the index is mapped back to a physical\n * line; a file that cannot be re-read yields the file alone rather than a\n * guessed line.\n * @param errors - Validation errors in the order the validator reported them\n * @returns Location of the first error, or undefined when there is none\n */\nexport async function firstErrorLocation(\n errors: readonly ValidationErrorDetail[],\n): Promise<{ file: string; line?: number } | undefined> {\n const first = errors[0];\n if (!first) return undefined;\n try {\n const content = await readFile(first.file, \"utf-8\");\n return { file: first.file, line: physicalLineOfRecord(content, first.rowIndex) };\n } catch {\n return { file: first.file };\n }\n}\n","/**\n * Seed integration module for generated seed code.\n *\n * Re-exports `@toiroakr/lines-db` through a single import path\n * to avoid phantom dependency issues with pnpm, and provides\n * seed-specific utility functions used by the code generator.\n */\n\nimport { readdir, stat } from \"node:fs/promises\";\nimport {\n LinesDB,\n ErrorFormatter,\n fillFields,\n findSchemaFile,\n isSchemaFile,\n unwrap,\n} from \"@toiroakr/lines-db\";\n// `pathe`, not `node:path`: the file paths reported back are printed and returned\n// to the caller, and these stay separator-stable across platforms.\nimport { basename, dirname, join, normalize } from \"pathe\";\nimport { firstErrorLocation } from \"./record-lines\";\nimport type { JsonlParseError, RowFiller, ValidationErrorDetail } from \"@toiroakr/lines-db\";\n\nexport { defineSchema } from \"@toiroakr/lines-db\";\nexport type { ForeignKeyDefinition, IndexDefinition } from \"@toiroakr/lines-db\";\n\n/** Fields `fillSeedData` writes when the caller names none. */\nconst DEFAULT_FILL_FIELDS = [\"id\"];\n\ntype SeedHook = RowFiller;\n\ntype SeedDataTarget = {\n dataDir: string;\n tableName?: string;\n};\n\ntype ValidateSeedDataOptions = {\n /** Resolved absolute path to a data directory or a .jsonl file */\n path: string;\n /**\n * Dataset directories whose rows are validated on top of the rows in `path`, which must then be\n * the data directory holding the schema files. A table's rows are those of every directory, in\n * this order, so foreign keys and duplicate ids are checked across them.\n */\n datasetDirs?: readonly string[];\n /** Validate only the rows of `datasetDirs`, leaving the rows in `path` out. Defaults to `false`. */\n excludeBase?: boolean;\n /** Show verbose error output */\n verbose?: boolean;\n};\n\ntype ValidateSeedResult =\n | { valid: true; output: string }\n | {\n valid: false;\n output: string;\n error: string;\n /** Where the first reported error sits, for tooling that links to source. */\n location?: { file: string; line?: number };\n };\n\n/**\n * A JSONL seed file that received values.\n */\ntype FilledSeedFile = {\n /** Name of the seeded table, matching the JSONL file name */\n table: string;\n /** Absolute path to the updated JSONL file */\n file: string;\n /** Fields that were written back to the file */\n fields: string[];\n /** How many rows were missing at least one of those fields */\n count: number;\n};\n\ntype FillSeedDataOptions = {\n /** Resolved absolute path to a data directory or a .jsonl file */\n path: string;\n /** Fields to fill. Defaults to `id`. */\n fields?: readonly string[];\n /** Directory the schema files are looked up in. Defaults to the directory of the data. */\n schemaDir?: string;\n};\n\ntype FillSeedDataResult = {\n output: string;\n filled: FilledSeedFile[];\n};\n\nasync function hasSchemaFiles(dir: string): Promise<boolean> {\n try {\n return (await readdir(dir)).some((entry) => isSchemaFile(entry));\n } catch {\n return false;\n }\n}\n\n/**\n * Point at `--dataset` for a directory without schema files of its own inside one that has them:\n * named as a path, it is checked on its own, not as a dataset on top of the seed data.\n * @param dataDir - Directory of the data the command was pointed at\n * @param command - The `tailor seed` command to suggest\n * @returns The hint as a warning, or none when the directory does not look like a dataset\n */\nasync function datasetHint(dataDir: string, command: \"validate\" | \"fill\"): Promise<string[]> {\n const parent = dirname(dataDir);\n if (parent === dataDir || (await hasSchemaFiles(dataDir)) || !(await hasSchemaFiles(parent))) {\n return [];\n }\n return [\n `${dataDir} has no schema files of its own. To ${command} it as a dataset with the schema files ` +\n `in ${parent}, run \\`tailor seed ${command} --dataset ${basename(dataDir)}\\`.`,\n ];\n}\n\nasync function resolveSeedDataTarget(resolvedPath: string): Promise<SeedDataTarget> {\n const stats = await stat(resolvedPath);\n if (stats.isDirectory()) {\n return { dataDir: resolvedPath };\n }\n if (stats.isFile() && resolvedPath.endsWith(\".jsonl\")) {\n return { dataDir: dirname(resolvedPath), tableName: basename(resolvedPath, \".jsonl\") };\n }\n throw new Error(`Invalid path: ${resolvedPath}. Must be a directory or .jsonl file.`);\n}\n\ntype ValidationConfig = {\n dataDir: string | string[];\n schemaDir?: string;\n tableName?: string;\n /** Warnings about how the path was resolved */\n warnings: string[];\n};\n\nasync function resolveValidationConfig(\n options: ValidateSeedDataOptions,\n): Promise<ValidationConfig> {\n const { path: resolvedPath, datasetDirs = [], excludeBase = false } = options;\n if (datasetDirs.length === 0) {\n if (excludeBase) {\n throw new Error(\"Leaving the base data out needs at least one dataset to validate.\");\n }\n const { dataDir, tableName } = await resolveSeedDataTarget(resolvedPath);\n return { dataDir, tableName, warnings: await datasetHint(dataDir, \"validate\") };\n }\n if (!(await stat(resolvedPath)).isDirectory()) {\n throw new Error(\n `Invalid path: ${resolvedPath}. Datasets are validated against the schema files of a data directory, so the path must be a directory.`,\n );\n }\n const dataDirs = excludeBase ? [...datasetDirs] : [resolvedPath, ...datasetDirs];\n return {\n dataDir: dataDirs.length === 1 ? (dataDirs[0] as string) : dataDirs,\n schemaDir: resolvedPath,\n warnings: await findFilesWithoutSchema(datasetDirs, resolvedPath),\n };\n}\n\nasync function findFilesWithoutSchema(\n dirs: readonly string[],\n schemaDir: string,\n): Promise<string[]> {\n const warnings: string[] = [];\n for (const dir of dirs) {\n for (const table of await listSeedTables(dir)) {\n if (!(await findSchemaFile(schemaDir, table))) {\n warnings.push(\n `${join(dir, `${table}.jsonl`)} has no schema file in ${schemaDir}, so its rows are not checked`,\n );\n }\n }\n }\n return warnings;\n}\n\nconst MISSING_REFERENCED_TABLE =\n /^Foreign key reference to non-existent table '(.+)' in table '(.+)'$/;\n\nasync function listSeedTables(dataDir: string): Promise<string[]> {\n const entries = await readdir(dataDir);\n return entries\n .filter((entry) => entry.endsWith(\".jsonl\"))\n .map((entry) => basename(entry, \".jsonl\"))\n .toSorted();\n}\n\nfunction formatWarnings(warnings: string[]): string[] {\n if (warnings.length === 0) {\n return [];\n }\n return [...warnings.map((warning) => `⚠ ${warning}`), \"\"];\n}\n\nfunction isJsonlParseError(error: Error): error is JsonlParseError {\n return error.name === \"JsonlParseError\";\n}\n\nfunction formatValidationErrors(errors: ValidationErrorDetail[], verbose: boolean): string {\n const formatter = new ErrorFormatter({ verbose });\n const errorLines: string[] = [];\n const errorsByFile = new Map<string, ValidationErrorDetail[]>();\n for (const error of errors) {\n const fileErrors = errorsByFile.get(error.file) || [];\n fileErrors.push(error);\n errorsByFile.set(error.file, fileErrors);\n }\n for (const [file, fileErrors] of errorsByFile) {\n errorLines.push(formatter.formatErrorHeader(fileErrors.length, file));\n errorLines.push(\"\");\n const validationErrors = fileErrors.filter(\n (e) => e.type !== \"foreignKey\" || !e.foreignKeyError,\n );\n const foreignKeyErrors = fileErrors.filter((e) => e.type === \"foreignKey\" && e.foreignKeyError);\n if (validationErrors.length > 0) {\n errorLines.push(\n formatter.formatValidationErrors(\n validationErrors.map((e) => ({\n file: e.file,\n rowIndex: e.rowIndex,\n issues: e.issues,\n })),\n ),\n );\n }\n for (const fkError of foreignKeyErrors) {\n if (fkError.foreignKeyError) {\n errorLines.push(\n formatter.formatForeignKeyError({\n file: fkError.file,\n rowIndex: fkError.rowIndex,\n column: fkError.foreignKeyError.column,\n value: fkError.foreignKeyError.value,\n referencedTable: fkError.foreignKeyError.referencedTable,\n referencedColumn: fkError.foreignKeyError.referencedColumn,\n }),\n );\n }\n }\n errorLines.push(\"\");\n }\n\n return errorLines.join(\"\\n\");\n}\n\n/**\n * Validate JSONL seed data against schema definitions.\n * Resolves the given path (directory or `.jsonl` file), validates the rows it\n * holds, and returns formatted output and error messages. With `datasetDirs`,\n * the rows of those directories are validated on top of the rows in `path`,\n * against the schema files in `path`.\n * @param options - Validation options including path, datasets, and verbose flag\n * @returns Validation result with output messages and optional error details\n */\nexport async function validateSeedData(\n options: ValidateSeedDataOptions,\n): Promise<ValidateSeedResult> {\n const { verbose = false, excludeBase = false } = options;\n const { tableName, warnings, ...config } = await resolveValidationConfig(options);\n\n const db = LinesDB.create(config);\n let initialized;\n try {\n initialized = await db.initialize({ tableName, detailedValidate: true });\n } finally {\n unwrap(await db.close());\n }\n if (!initialized.ok && isJsonlParseError(initialized.error)) {\n const { file, line, message } = initialized.error;\n return {\n valid: false,\n output: \"\",\n error: [\n new ErrorFormatter({ verbose }).formatErrorHeader(1, file),\n \"\",\n ` Line ${line}: ${message}`,\n \"\",\n ].join(\"\\n\"),\n location: { file, line },\n };\n }\n if (excludeBase && !initialized.ok) {\n const missingTable = MISSING_REFERENCED_TABLE.exec(initialized.error.message);\n if (missingTable) {\n const [, referencedTable, table] = missingTable;\n throw new Error(\n `${table} has a foreign key to ${referencedTable}, but none of the selected datasets has ${referencedTable}.jsonl. ` +\n \"Add the file to one of them, even an empty one, or validate them on top of the base seed data.\",\n { cause: initialized.error },\n );\n }\n }\n const result = unwrap(initialized);\n\n const outputLines = formatWarnings([...warnings, ...result.warnings]);\n\n if (result.valid) {\n outputLines.push(\"✓ All records are valid\");\n return { valid: true, output: outputLines.join(\"\\n\") };\n }\n\n return {\n valid: false,\n output: outputLines.join(\"\\n\"),\n error: formatValidationErrors(result.errors, verbose),\n location: await firstErrorLocation(result.errors),\n };\n}\n\n/**\n * Fill in the values a record gets on create for the JSONL seed data rows that\n * are missing them, so a row can be referenced by `id` or carry a timestamp\n * before it is ever seeded.\n *\n * The values come from the table's own create-time behavior — its `id`, its field\n * defaults, and its create hooks — applied to each row on its own. Nothing is\n * validated, so a row can be filled while the data around it is still\n * incomplete: that is what lets you get the ids you need in order to write the\n * rows that reference them. Run `validateSeedData` when the data is ready.\n *\n * Only the named fields are written, and only into a row that has no value for\n * them, so a value already in the file is never replaced. A line that gains\n * nothing is written back exactly as it was, byte for byte; a line that does get\n * a value is re-serialized with its keys in the order the table declares its\n * fields, so a filled-in `id` lands at the front. A field the table gives no\n * value to — one it does not declare, or one the platform assigns such as a\n * serial field — is skipped, so one field list covers a whole data directory.\n *\n * The values are read from the schema files generated next to the data, or in\n * `schemaDir` when it is given, and all\n * of them are read before anything is written: a file that predates the current\n * generator stops the run with nothing filled in anywhere. Likewise, a file that\n * another tool changed after the fill read it is not overwritten: the call\n * rejects with an error naming that file, and no file is written.\n * @param options - Fill options including path and fields\n * @returns Which files received which fields\n */\nexport async function fillSeedData(options: FillSeedDataOptions): Promise<FillSeedDataResult> {\n const { path: resolvedPath, fields = DEFAULT_FILL_FIELDS } = options;\n if (fields.length === 0) {\n throw new Error(\"No fields to fill. Name at least one field.\");\n }\n const { dataDir, tableName } = await resolveSeedDataTarget(resolvedPath);\n const tables = tableName ? [tableName] : await listSeedTables(dataDir);\n\n const result = unwrap(\n await fillFields({\n path: resolvedPath,\n fields,\n schemaDir: options.schemaDir,\n loadFiller: (schemaModule, { schemaPath }) => {\n const hook = schemaModule.hook;\n if (typeof hook !== \"function\") {\n throw new Error(\n `${schemaPath} does not export \\`hook\\`. Run \\`tailor generate\\` to regenerate the seed schema files.`,\n );\n }\n return hook as SeedHook;\n },\n }),\n );\n const { tablesWithoutSchema, unproducedFields } = result;\n const filled = result.filled.map((entry) => ({ ...entry, file: normalize(entry.file) }));\n const unreadableLines = result.unreadableLines.map((entry) => ({\n ...entry,\n file: normalize(entry.file),\n }));\n\n const warnings = [\n ...(options.schemaDir ? [] : await datasetHint(dataDir, \"fill\")),\n ...tablesWithoutSchema.map(\n (table) => `No schema file for ${table}, so nothing can be filled in there`,\n ),\n ...unreadableLines.map(\n ({ file, lines }) =>\n `${file}: line(s) ${lines.join(\", \")} are not JSON objects, so nothing was filled in there`,\n ),\n ];\n if (tables.length > 0 && unproducedFields.length > 0) {\n warnings.push(`No seed data produces a value for: ${unproducedFields.join(\", \")}`);\n }\n\n const outputLines = formatWarnings(warnings);\n outputLines.push(\n filled.length === 0\n ? \"\\u2713 Nothing to fill\"\n : filled\n .map(\n ({ file, fields: tableFields, count }) =>\n `\\u2713 ${file}: filled ${tableFields.join(\", \")} in ${count} row(s)`,\n )\n .join(\"\\n\"),\n );\n\n return { output: outputLines.join(\"\\n\"), filled };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["lines","content","remaining","recordIndex","index","line","first","errors","readFile","DEFAULT_FILL_FIELDS","readdir","dir","isSchemaFile","entry","parent","dirname","dataDir","command","basename","stats","stat","resolvedPath","options","datasetDirs","excludeBase","dataDirs","warnings","dirs","table","findSchemaFile","schemaDir","join","MISSING_REFERENCED_TABLE","warning","error","formatter","ErrorFormatter","errorLines","errorsByFile","fileErrors","file","validationErrors","foreignKeyErrors","fkError","config","db","LinesDB","initialized","unwrap","message","missingTable","referencedTable","result","outputLines","verbose","fields","tables","tableName","fillFields","hook","schemaModule","schemaPath","filled","normalize","unreadableLines","tablesWithoutSchema","unproducedFields","tableFields","count","defineSchema"],"sources":["../../src/seed/record-lines.ts","../../src/seed/index.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { ValidationErrorDetail } from \"lines-db\";\n\n/**\n * Resolve the 1-based physical line a JSONL record occupies.\n *\n * The seed reader drops blank and whitespace-only lines before indexing its\n * records, so a record's index is not its line number once a file contains\n * one. Reported locations have to point at the line a reader would open.\n * @param content - Raw JSONL file contents\n * @param recordIndex - 0-based index among the file's non-blank lines\n * @returns 1-based physical line, or undefined when the file holds no such record\n */\nexport function physicalLineOfRecord(content: string, recordIndex: number): number | undefined {\n const lines = content.split(\"\\n\");\n let remaining = recordIndex;\n for (const [index, line] of lines.entries()) {\n if (line.trim().length === 0) continue;\n if (remaining === 0) return index + 1;\n remaining -= 1;\n }\n return undefined;\n}\n\n/**\n * Locate the first reported error in its own file.\n *\n * Errors are reported per record, so the index is mapped back to a physical\n * line; a file that cannot be re-read yields the file alone rather than a\n * guessed line.\n * @param errors - Validation errors in the order the validator reported them\n * @returns Location of the first error, or undefined when there is none\n */\nexport async function firstErrorLocation(\n errors: readonly ValidationErrorDetail[],\n): Promise<{ file: string; line?: number } | undefined> {\n const first = errors[0];\n if (!first) return undefined;\n try {\n const content = await readFile(first.file, \"utf-8\");\n return { file: first.file, line: physicalLineOfRecord(content, first.rowIndex) };\n } catch {\n return { file: first.file };\n }\n}\n","/**\n * Seed integration module for generated seed code.\n *\n * Re-exports `lines-db` through a single import path\n * to avoid phantom dependency issues with pnpm, and provides\n * seed-specific utility functions used by the code generator.\n */\n\nimport { readdir, stat } from \"node:fs/promises\";\nimport {\n LinesDB,\n ErrorFormatter,\n fillFields,\n findSchemaFile,\n isSchemaFile,\n unwrap,\n} from \"lines-db\";\n// `pathe`, not `node:path`: the file paths reported back are printed and returned\n// to the caller, and these stay separator-stable across platforms.\nimport { basename, dirname, join, normalize } from \"pathe\";\nimport { firstErrorLocation } from \"./record-lines\";\nimport type { JsonlParseError, RowFiller, ValidationErrorDetail } from \"lines-db\";\n\nexport { defineSchema } from \"lines-db\";\nexport type { ForeignKeyDefinition, IndexDefinition } from \"lines-db\";\n\n/** Fields `fillSeedData` writes when the caller names none. */\nconst DEFAULT_FILL_FIELDS = [\"id\"];\n\ntype SeedHook = RowFiller;\n\ntype SeedDataTarget = {\n dataDir: string;\n tableName?: string;\n};\n\ntype ValidateSeedDataOptions = {\n /** Resolved absolute path to a data directory or a .jsonl file */\n path: string;\n /**\n * Dataset directories whose rows are validated on top of the rows in `path`, which must then be\n * the data directory holding the schema files. A table's rows are those of every directory, in\n * this order, so foreign keys and duplicate ids are checked across them.\n */\n datasetDirs?: readonly string[];\n /** Validate only the rows of `datasetDirs`, leaving the rows in `path` out. Defaults to `false`. */\n excludeBase?: boolean;\n /** Show verbose error output */\n verbose?: boolean;\n};\n\ntype ValidateSeedResult =\n | { valid: true; output: string }\n | {\n valid: false;\n output: string;\n error: string;\n /** Where the first reported error sits, for tooling that links to source. */\n location?: { file: string; line?: number };\n };\n\n/**\n * A JSONL seed file that received values.\n */\ntype FilledSeedFile = {\n /** Name of the seeded table, matching the JSONL file name */\n table: string;\n /** Absolute path to the updated JSONL file */\n file: string;\n /** Fields that were written back to the file */\n fields: string[];\n /** How many rows were missing at least one of those fields */\n count: number;\n};\n\ntype FillSeedDataOptions = {\n /** Resolved absolute path to a data directory or a .jsonl file */\n path: string;\n /** Fields to fill. Defaults to `id`. */\n fields?: readonly string[];\n /** Directory the schema files are looked up in. Defaults to the directory of the data. */\n schemaDir?: string;\n};\n\ntype FillSeedDataResult = {\n output: string;\n filled: FilledSeedFile[];\n};\n\nasync function hasSchemaFiles(dir: string): Promise<boolean> {\n try {\n return (await readdir(dir)).some((entry) => isSchemaFile(entry));\n } catch {\n return false;\n }\n}\n\n/**\n * Point at `--dataset` for a directory without schema files of its own inside one that has them:\n * named as a path, it is checked on its own, not as a dataset on top of the seed data.\n * @param dataDir - Directory of the data the command was pointed at\n * @param command - The `tailor seed` command to suggest\n * @returns The hint as a warning, or none when the directory does not look like a dataset\n */\nasync function datasetHint(dataDir: string, command: \"validate\" | \"fill\"): Promise<string[]> {\n const parent = dirname(dataDir);\n if (parent === dataDir || (await hasSchemaFiles(dataDir)) || !(await hasSchemaFiles(parent))) {\n return [];\n }\n return [\n `${dataDir} has no schema files of its own. To ${command} it as a dataset with the schema files ` +\n `in ${parent}, run \\`tailor seed ${command} --dataset ${basename(dataDir)}\\`.`,\n ];\n}\n\nasync function resolveSeedDataTarget(resolvedPath: string): Promise<SeedDataTarget> {\n const stats = await stat(resolvedPath);\n if (stats.isDirectory()) {\n return { dataDir: resolvedPath };\n }\n if (stats.isFile() && resolvedPath.endsWith(\".jsonl\")) {\n return { dataDir: dirname(resolvedPath), tableName: basename(resolvedPath, \".jsonl\") };\n }\n throw new Error(`Invalid path: ${resolvedPath}. Must be a directory or .jsonl file.`);\n}\n\ntype ValidationConfig = {\n dataDir: string | string[];\n schemaDir?: string;\n tableName?: string;\n /** Warnings about how the path was resolved */\n warnings: string[];\n};\n\nasync function resolveValidationConfig(\n options: ValidateSeedDataOptions,\n): Promise<ValidationConfig> {\n const { path: resolvedPath, datasetDirs = [], excludeBase = false } = options;\n if (datasetDirs.length === 0) {\n if (excludeBase) {\n throw new Error(\"Leaving the base data out needs at least one dataset to validate.\");\n }\n const { dataDir, tableName } = await resolveSeedDataTarget(resolvedPath);\n return { dataDir, tableName, warnings: await datasetHint(dataDir, \"validate\") };\n }\n if (!(await stat(resolvedPath)).isDirectory()) {\n throw new Error(\n `Invalid path: ${resolvedPath}. Datasets are validated against the schema files of a data directory, so the path must be a directory.`,\n );\n }\n const dataDirs = excludeBase ? [...datasetDirs] : [resolvedPath, ...datasetDirs];\n return {\n dataDir: dataDirs.length === 1 ? (dataDirs[0] as string) : dataDirs,\n schemaDir: resolvedPath,\n warnings: await findFilesWithoutSchema(datasetDirs, resolvedPath),\n };\n}\n\nasync function findFilesWithoutSchema(\n dirs: readonly string[],\n schemaDir: string,\n): Promise<string[]> {\n const warnings: string[] = [];\n for (const dir of dirs) {\n for (const table of await listSeedTables(dir)) {\n if (!(await findSchemaFile(schemaDir, table))) {\n warnings.push(\n `${join(dir, `${table}.jsonl`)} has no schema file in ${schemaDir}, so its rows are not checked`,\n );\n }\n }\n }\n return warnings;\n}\n\nconst MISSING_REFERENCED_TABLE =\n /^Foreign key reference to non-existent table '(.+)' in table '(.+)'$/;\n\nasync function listSeedTables(dataDir: string): Promise<string[]> {\n const entries = await readdir(dataDir);\n return entries\n .filter((entry) => entry.endsWith(\".jsonl\"))\n .map((entry) => basename(entry, \".jsonl\"))\n .toSorted();\n}\n\nfunction formatWarnings(warnings: string[]): string[] {\n if (warnings.length === 0) {\n return [];\n }\n return [...warnings.map((warning) => `⚠ ${warning}`), \"\"];\n}\n\nfunction isJsonlParseError(error: Error): error is JsonlParseError {\n return error.name === \"JsonlParseError\";\n}\n\nfunction formatValidationErrors(errors: ValidationErrorDetail[], verbose: boolean): string {\n const formatter = new ErrorFormatter({ verbose });\n const errorLines: string[] = [];\n const errorsByFile = new Map<string, ValidationErrorDetail[]>();\n for (const error of errors) {\n const fileErrors = errorsByFile.get(error.file) || [];\n fileErrors.push(error);\n errorsByFile.set(error.file, fileErrors);\n }\n for (const [file, fileErrors] of errorsByFile) {\n errorLines.push(formatter.formatErrorHeader(fileErrors.length, file));\n errorLines.push(\"\");\n const validationErrors = fileErrors.filter(\n (e) => e.type !== \"foreignKey\" || !e.foreignKeyError,\n );\n const foreignKeyErrors = fileErrors.filter((e) => e.type === \"foreignKey\" && e.foreignKeyError);\n if (validationErrors.length > 0) {\n errorLines.push(\n formatter.formatValidationErrors(\n validationErrors.map((e) => ({\n file: e.file,\n rowIndex: e.rowIndex,\n issues: e.issues,\n })),\n ),\n );\n }\n for (const fkError of foreignKeyErrors) {\n if (fkError.foreignKeyError) {\n errorLines.push(\n formatter.formatForeignKeyError({\n file: fkError.file,\n rowIndex: fkError.rowIndex,\n column: fkError.foreignKeyError.column,\n value: fkError.foreignKeyError.value,\n referencedTable: fkError.foreignKeyError.referencedTable,\n referencedColumn: fkError.foreignKeyError.referencedColumn,\n }),\n );\n }\n }\n errorLines.push(\"\");\n }\n\n return errorLines.join(\"\\n\");\n}\n\n/**\n * Validate JSONL seed data against schema definitions.\n * Resolves the given path (directory or `.jsonl` file), validates the rows it\n * holds, and returns formatted output and error messages. With `datasetDirs`,\n * the rows of those directories are validated on top of the rows in `path`,\n * against the schema files in `path`.\n * @param options - Validation options including path, datasets, and verbose flag\n * @returns Validation result with output messages and optional error details\n */\nexport async function validateSeedData(\n options: ValidateSeedDataOptions,\n): Promise<ValidateSeedResult> {\n const { verbose = false, excludeBase = false } = options;\n const { tableName, warnings, ...config } = await resolveValidationConfig(options);\n\n const db = LinesDB.create(config);\n let initialized;\n try {\n initialized = await db.initialize({ tableName, detailedValidate: true });\n } finally {\n unwrap(await db.close());\n }\n if (!initialized.ok && isJsonlParseError(initialized.error)) {\n const { file, line, message } = initialized.error;\n return {\n valid: false,\n output: \"\",\n error: [\n new ErrorFormatter({ verbose }).formatErrorHeader(1, file),\n \"\",\n ` Line ${line}: ${message}`,\n \"\",\n ].join(\"\\n\"),\n location: { file, line },\n };\n }\n if (excludeBase && !initialized.ok) {\n const missingTable = MISSING_REFERENCED_TABLE.exec(initialized.error.message);\n if (missingTable) {\n const [, referencedTable, table] = missingTable;\n throw new Error(\n `${table} has a foreign key to ${referencedTable}, but none of the selected datasets has ${referencedTable}.jsonl. ` +\n \"Add the file to one of them, even an empty one, or validate them on top of the base seed data.\",\n { cause: initialized.error },\n );\n }\n }\n const result = unwrap(initialized);\n\n const outputLines = formatWarnings([...warnings, ...result.warnings]);\n\n if (result.valid) {\n outputLines.push(\"✓ All records are valid\");\n return { valid: true, output: outputLines.join(\"\\n\") };\n }\n\n return {\n valid: false,\n output: outputLines.join(\"\\n\"),\n error: formatValidationErrors(result.errors, verbose),\n location: await firstErrorLocation(result.errors),\n };\n}\n\n/**\n * Fill in the values a record gets on create for the JSONL seed data rows that\n * are missing them, so a row can be referenced by `id` or carry a timestamp\n * before it is ever seeded.\n *\n * The values come from the table's own create-time behavior — its `id`, its field\n * defaults, and its create hooks — applied to each row on its own. Nothing is\n * validated, so a row can be filled while the data around it is still\n * incomplete: that is what lets you get the ids you need in order to write the\n * rows that reference them. Run `validateSeedData` when the data is ready.\n *\n * Only the named fields are written, and only into a row that has no value for\n * them, so a value already in the file is never replaced. A line that gains\n * nothing is written back exactly as it was, byte for byte; a line that does get\n * a value is re-serialized with its keys in the order the table declares its\n * fields, so a filled-in `id` lands at the front. A field the table gives no\n * value to — one it does not declare, or one the platform assigns such as a\n * serial field — is skipped, so one field list covers a whole data directory.\n *\n * The values are read from the schema files generated next to the data, or in\n * `schemaDir` when it is given, and all\n * of them are read before anything is written: a file that predates the current\n * generator stops the run with nothing filled in anywhere. Likewise, a file that\n * another tool changed after the fill read it is not overwritten: the call\n * rejects with an error naming that file, and no file is written.\n * @param options - Fill options including path and fields\n * @returns Which files received which fields\n */\nexport async function fillSeedData(options: FillSeedDataOptions): Promise<FillSeedDataResult> {\n const { path: resolvedPath, fields = DEFAULT_FILL_FIELDS } = options;\n if (fields.length === 0) {\n throw new Error(\"No fields to fill. Name at least one field.\");\n }\n const { dataDir, tableName } = await resolveSeedDataTarget(resolvedPath);\n const tables = tableName ? [tableName] : await listSeedTables(dataDir);\n\n const result = unwrap(\n await fillFields({\n path: resolvedPath,\n fields,\n schemaDir: options.schemaDir,\n loadFiller: (schemaModule, { schemaPath }) => {\n const hook = schemaModule.hook;\n if (typeof hook !== \"function\") {\n throw new Error(\n `${schemaPath} does not export \\`hook\\`. Run \\`tailor generate\\` to regenerate the seed schema files.`,\n );\n }\n return hook as SeedHook;\n },\n }),\n );\n const { tablesWithoutSchema, unproducedFields } = result;\n const filled = result.filled.map((entry) => ({ ...entry, file: normalize(entry.file) }));\n const unreadableLines = result.unreadableLines.map((entry) => ({\n ...entry,\n file: normalize(entry.file),\n }));\n\n const warnings = [\n ...(options.schemaDir ? [] : await datasetHint(dataDir, \"fill\")),\n ...tablesWithoutSchema.map(\n (table) => `No schema file for ${table}, so nothing can be filled in there`,\n ),\n ...unreadableLines.map(\n ({ file, lines }) =>\n `${file}: line(s) ${lines.join(\", \")} are not JSON objects, so nothing was filled in there`,\n ),\n ];\n if (tables.length > 0 && unproducedFields.length > 0) {\n warnings.push(`No seed data produces a value for: ${unproducedFields.join(\", \")}`);\n }\n\n const outputLines = formatWarnings(warnings);\n outputLines.push(\n filled.length === 0\n ? \"\\u2713 Nothing to fill\"\n : filled\n .map(\n ({ file, fields: tableFields, count }) =>\n `\\u2713 ${file}: filled ${tableFields.join(\", \")} in ${count} row(s)`,\n )\n .join(\"\\n\"),\n );\n\n return { output: outputLines.join(\"\\n\"), filled };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed-C3P_T_Eh.mjs","names":["assertDefined","isPluginGeneratedTable","path","auth","tableName","idpNamespace","listIdpUsersFunction","options","schemaBody","includeUserProfileFK","userTableName","usernameField","source","type","field","optionalFields","omitFields","indexes","foreignKeys","fieldName","indexName","indexDef","schemaOptions","fk","index","option","tableVariable","metadata","params","schemaOptionsCode","configImportPath","exportName","typeImportPath","pluginSource","schemaTypeCode","originalImportPath","SeedGeneratorID","files","idpUser","ctx","idpUserSyncFKs","ns","linesDb","schemaOutputPath","relativePath","schemaContent"],"sources":["../src/plugin/builtin/seed/idp-user-processor.ts","../src/plugin/builtin/seed/lines-db-processor.ts","../src/plugin/builtin/seed/index.ts"],"sourcesContent":["import ml from \"#/utils/multiline\";\nimport type { GeneratorAuthInput } from \"#/plugin/types\";\n\nexport interface IdpUserMetadata {\n name: \"_User\";\n dependencies: string[];\n dataFile: string;\n idpNamespace: string;\n schema: {\n usernameField: string;\n userTableName: string;\n };\n}\n\n/**\n * Processes auth configuration to generate IdP user seed metadata\n * @param auth - Auth configuration from generator\n * @returns IdP user metadata or undefined if not applicable\n */\nexport function processIdpUser(auth: GeneratorAuthInput): IdpUserMetadata | undefined {\n // Only process if idProvider is BuiltInIdP and userProfile is defined\n if (auth.idProvider?.kind !== \"BuiltInIdP\" || !auth.userProfile) {\n return undefined;\n }\n\n const { tableName, usernameField } = auth.userProfile;\n\n return {\n name: \"_User\",\n dependencies: [tableName],\n dataFile: \"data/_User.jsonl\",\n idpNamespace: auth.idProvider.namespace,\n schema: {\n usernameField,\n userTableName: tableName,\n },\n };\n}\n\n/**\n * Generates the server-side IDP seed script code.\n * Uses the global tailor.idp.Client - no bundling required.\n * @param idpNamespace - The IDP namespace name\n * @returns Script code string\n */\nexport function generateIdpSeedScriptCode(idpNamespace: string): string {\n return ml /* ts */ `\n export async function main(input) {\n const client = new tailor.idp.Client({ namespace: \"${idpNamespace}\" });\n const errors = [];\n let processed = 0;\n let created = 0;\n let updated = 0;\n let skipped = 0;\n const upsert = input.upsert === true;\n // Rows may arrive as one chunk of a larger dataset; report row numbers\n // relative to the whole dataset so they match the source JSONL.\n const offset = typeof input.offset === \"number\" ? input.offset : 0;\n const total = typeof input.total === \"number\" ? input.total : input.users.length;\n\n for (let i = 0; i < input.users.length; i++) {\n try {\n if (upsert) {\n let existing;\n let lookupError;\n try {\n existing = await client.userByName(input.users[i].name);\n } catch (error) {\n existing = undefined;\n lookupError = error instanceof Error ? error.message : String(error);\n }\n\n if (existing) {\n const { name, ...attributes } = input.users[i];\n if (Object.keys(attributes).length === 0) {\n skipped++;\n } else {\n await client.updateUser({ id: existing.id, ...attributes });\n updated++;\n }\n } else {\n try {\n await client.createUser(input.users[i]);\n created++;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n lookupError ? \\`create failed (\\${message}); lookup failed (\\${lookupError})\\` : message,\n );\n }\n }\n } else {\n await client.createUser(input.users[i]);\n created++;\n }\n processed++;\n console.log(\\`[_User] \\${offset + i + 1}/\\${total}: \\${input.users[i].name}\\`);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n errors.push(\\`Row \\${offset + i + 1} (\\${input.users[i].name}): \\${message}\\`);\n console.error(\\`[_User] Row \\${offset + i + 1} failed: \\${message}\\`);\n }\n }\n\n return {\n success: errors.length === 0,\n processed,\n created,\n updated,\n skipped,\n errors,\n };\n }\n `;\n}\n\nconst listIdpUsersFunction = ml /* ts */ `\n async function listUsers(client) {\n let after = undefined;\n const users = [];\n do {\n const response = await client.users(after ? { after } : undefined);\n for (const user of response.users || []) {\n users.push({ id: user.id, name: user.name });\n }\n after = response.nextPageToken;\n } while (after);\n console.log(\\`Found \\${users.length} IDP users to delete\\`);\n return users;\n }\n`;\n\n/**\n * Generates the server-side script that lists every IdP user for truncation.\n * @param idpNamespace - The IDP namespace name\n * @returns Script code string\n */\nexport function generateIdpListUsersScriptCode(idpNamespace: string): string {\n return ml /* ts */ `\n ${listIdpUsersFunction}\n\n export async function main() {\n const client = new tailor.idp.Client({ namespace: \"${idpNamespace}\" });\n const users = await listUsers(client);\n return { success: true, users };\n }\n `;\n}\n\n/**\n * Generates the server-side IDP truncation script code.\n * Deletes the users passed in `input.users` (one chunk of the listing produced by\n * {@link generateIdpListUsersScriptCode}), or every user when no chunk is passed so\n * older seed plugins that call it without input keep working. A user that is already\n * gone counts as deleted.\n * @param idpNamespace - The IDP namespace name\n * @returns Script code string\n */\nexport function generateIdpTruncateScriptCode(idpNamespace: string): string {\n return ml /* ts */ `\n ${listIdpUsersFunction}\n\n export async function main(input) {\n const client = new tailor.idp.Client({ namespace: \"${idpNamespace}\" });\n const errors = [];\n let deleted = 0;\n let notFound = 0;\n const users = Array.isArray(input.users) ? input.users : await listUsers(client);\n const offset = typeof input.offset === \"number\" ? input.offset : 0;\n const total = typeof input.total === \"number\" ? input.total : users.length;\n\n for (let i = 0; i < users.length; i++) {\n const user = users[i];\n try {\n await client.deleteUser(user.id);\n deleted++;\n console.log(\\`[_User] Deleted \\${offset + i + 1}/\\${total}: \\${user.name}\\`);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (/user not found/i.test(message)) {\n notFound++;\n console.log(\\`[_User] Already deleted \\${offset + i + 1}/\\${total}: \\${user.name}\\`);\n continue;\n }\n errors.push(\\`User \\${user.id} (\\${user.name}): \\${message}\\`);\n console.error(\\`[_User] Delete failed for \\${user.name}: \\${message}\\`);\n }\n }\n\n return {\n success: errors.length === 0,\n deleted,\n notFound,\n total: users.length,\n errors,\n };\n }\n `;\n}\n\ntype GenerateIdpUserSchemaFileOptions = {\n usernameField: string;\n userTableName: string;\n /**\n * When `true` (default), emit a foreign key from `_User.name` to the\n * userProfile table's username field so that seed validation rejects `_User`\n * rows without a matching userProfile row. Set to `false` to seed `_User`\n * rows that do not yet have a corresponding userProfile row.\n */\n includeUserProfileFK?: boolean;\n};\n\n/**\n * Generates the schema file content for IdP users. Emits the\n * `_User.name -> <userProfile>.<usernameField>` foreign key by default; pass\n * `includeUserProfileFK: false` to omit it (e.g. when seeding `_User` rows\n * that do not yet have a corresponding userProfile row).\n * @param options - Schema generation options\n * @param options.usernameField - Username field name\n * @param options.userTableName - TailorDB user table name\n * @param options.includeUserProfileFK - Whether to emit the `_User -> userProfile` foreign key (default `true`)\n * @returns Schema file contents\n */\nexport function generateIdpUserSchemaFile(options: GenerateIdpUserSchemaFileOptions): string {\n const { usernameField, userTableName, includeUserProfileFK = true } = options;\n const schemaBody = includeUserProfileFK\n ? ml`\n primaryKey: \"name\",\n indexes: [\n { name: \"_user_name_unique_idx\", columns: [\"name\"], unique: true },\n ],\n foreignKeys: [\n {\n column: \"name\",\n references: {\n table: \"${userTableName}\",\n column: \"${usernameField}\",\n },\n },\n ],\n `\n : ml`\n primaryKey: \"name\",\n indexes: [\n { name: \"_user_name_unique_idx\", columns: [\"name\"], unique: true },\n ],\n `;\n\n return ml /* ts */ `\n import { t } from \"@tailor-platform/sdk\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createStandardSchema } from \"@tailor-platform/sdk/test\";\n\n const schemaType = t.object({\n name: t.string(),\n password: t.string(),\n });\n\n // Simple identity hook for _User (no TailorDB backing table)\n export const hook = <T>(data: unknown) => data as T;\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook),\n {\n ${schemaBody}\n }\n );\n\n `;\n}\n","import { isPluginGeneratedTable } from \"#/parser/service/tailordb/type-source\";\nimport ml from \"#/utils/multiline\";\nimport type {\n ParsedField,\n PluginGeneratedTableSource,\n TailorDBType,\n TypeSourceInfoEntry,\n} from \"#/parser/service/tailordb/types\";\nimport type { LinesDbMetadata } from \"./types\";\nimport type { ForeignKeyDefinition, IndexDefinition } from \"@toiroakr/lines-db\";\n\n/**\n * Processes TailorDB tables to generate lines-db metadata\n * @param type - Parsed TailorDB table\n * @param source - Source file info\n * @returns Generated lines-db metadata\n */\nexport function processLinesDb(type: TailorDBType, source: TypeSourceInfoEntry): LinesDbMetadata {\n if (isPluginGeneratedTable(source)) {\n // Plugin-generated table\n return processLinesDbForPluginTable(type, source);\n }\n\n // User-defined table\n if (!source.filePath) {\n throw new Error(`Missing source info for table ${type.name}`);\n }\n if (!source.exportName) {\n throw new Error(`Missing export name for table ${type.name}`);\n }\n\n const { optionalFields, omitFields, indexes, foreignKeys } = extractFieldMetadata(type);\n\n return {\n tableName: type.name,\n exportName: source.exportName,\n importPath: source.filePath,\n optionalFields,\n omitFields,\n foreignKeys,\n indexes,\n };\n}\n\n/**\n * Process lines-db metadata for plugin-generated tables\n * @param type - Parsed TailorDB table\n * @param source - Plugin-generated table source info\n * @returns Generated lines-db metadata with plugin source\n */\nfunction processLinesDbForPluginTable(\n type: TailorDBType,\n source: PluginGeneratedTableSource,\n): LinesDbMetadata {\n const { optionalFields, omitFields, indexes, foreignKeys } = extractFieldMetadata(type);\n\n return {\n tableName: type.name,\n exportName: source.exportName,\n importPath: \"\",\n optionalFields,\n omitFields,\n foreignKeys,\n indexes,\n pluginSource: source,\n };\n}\n\n/**\n * Whether the platform produces a value for the field on create when a row omits it,\n * either from a create hook or from a schema default. Such fields stay optional in seed\n * data even when the table marks them required.\n * @param field - Parsed TailorDB field\n * @returns True when a seed row does not have to supply the field\n */\nfunction isGeneratedOnCreate(field: ParsedField): boolean {\n return field.config.hooks?.create !== undefined || field.config.default !== undefined;\n}\n\n/**\n * Extract field metadata from TailorDB table\n * @param type - Parsed TailorDB table\n * @returns Field metadata including optional fields, omit fields, indexes, and foreign keys\n */\nfunction extractFieldMetadata(type: TailorDBType): {\n optionalFields: string[];\n omitFields: string[];\n indexes: IndexDefinition[];\n foreignKeys: ForeignKeyDefinition[];\n} {\n const optionalFields = [\"id\"]; // id is always optional\n const omitFields: string[] = [];\n const indexes: IndexDefinition[] = [];\n const foreignKeys: ForeignKeyDefinition[] = [];\n\n // Find fields generated on create, or serial\n for (const [fieldName, field] of Object.entries(type.fields)) {\n if (isGeneratedOnCreate(field)) {\n optionalFields.push(fieldName);\n }\n // Serial fields are auto-generated, so they are excluded from the seed schema entirely\n if (field.config.serial) {\n omitFields.push(fieldName);\n }\n if (field.config.unique) {\n indexes.push({\n name: `${type.name.toLowerCase()}_${fieldName}_unique_idx`,\n columns: [fieldName],\n unique: true,\n });\n }\n }\n\n // Extract indexes\n if (type.indexes) {\n for (const [indexName, indexDef] of Object.entries(type.indexes)) {\n indexes.push({\n name: indexName,\n columns: indexDef.fields,\n unique: indexDef.unique,\n });\n }\n }\n\n // Extract foreign keys from relations\n for (const [fieldName, field] of Object.entries(type.fields)) {\n if (field.relation) {\n foreignKeys.push({\n column: fieldName,\n references: {\n table: field.relation.targetType,\n column: field.relation.key,\n },\n });\n }\n }\n\n return { optionalFields, omitFields, indexes, foreignKeys };\n}\n\n/**\n * Generate schema options code for lines-db\n * @param foreignKeys - Foreign key definitions\n * @param indexes - Index definitions\n * @returns Schema options code string\n */\nfunction generateSchemaOptions(\n foreignKeys: ForeignKeyDefinition[],\n indexes: IndexDefinition[],\n): string {\n const schemaOptions: string[] = [];\n\n if (foreignKeys.length > 0) {\n schemaOptions.push(`foreignKeys: [`);\n foreignKeys.forEach((fk) => {\n schemaOptions.push(` ${JSON.stringify(fk)},`);\n });\n schemaOptions.push(`],`);\n }\n\n if (indexes.length > 0) {\n schemaOptions.push(`indexes: [`);\n indexes.forEach((index) => {\n schemaOptions.push(` ${JSON.stringify(index)},`);\n });\n schemaOptions.push(\"],\");\n }\n\n return schemaOptions.length > 0\n ? [\"\\n {\", ...schemaOptions.map((option) => ` ${option}`), \" }\"].join(\"\\n\")\n : \"\";\n}\n\n/**\n * Parameters for generating a user-defined table's schema file\n */\nexport interface UserTableSchemaParams {\n /** Relative import path to the table's source file */\n typeImportPath: string;\n /**\n * Relative path from the schema output to tailor.config.ts. Set when plugins are\n * attached to the table: the schema then loads the table through `getExtendedTable`\n * so the fields those plugins add are part of the seed schema.\n */\n configImportPath?: string;\n}\n\nfunction generateSchemaTypeCode(\n tableVariable: string,\n optionalFields: string[],\n omitFields: string[],\n): string {\n return ml /* ts */ `\n const schemaType = t.object({\n ...${tableVariable}.pickFields(${JSON.stringify(optionalFields)}, { optional: true }),\n ...${tableVariable}.omitFields(${JSON.stringify([...optionalFields, ...omitFields])}),\n });\n `;\n}\n\n/**\n * Generates the schema file content for lines-db (for user-defined tables with import)\n * @param metadata - lines-db metadata\n * @param params - Import paths for the table and, when plugins are attached, the config\n * @returns Schema file contents\n */\nexport function generateLinesDbSchemaFile(\n metadata: LinesDbMetadata,\n params: UserTableSchemaParams,\n): string {\n const { exportName, optionalFields, omitFields, foreignKeys, indexes } = metadata;\n const { typeImportPath, configImportPath } = params;\n\n const schemaOptionsCode = generateSchemaOptions(foreignKeys, indexes);\n\n if (configImportPath === undefined) {\n return ml /* ts */ `\n import { t } from \"@tailor-platform/sdk\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n import { ${exportName} } from \"${typeImportPath}\";\n\n ${generateSchemaTypeCode(exportName, optionalFields, omitFields)}\n\n export const hook = createTailorDBHook(${exportName});\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, ${exportName}),${schemaOptionsCode}\n );\n\n `;\n }\n\n // The source file exports the table without the fields its plugins add, so the\n // schema is built from the table `getExtendedTable` returns instead.\n return ml /* ts */ `\n import { join } from \"node:path\";\n import { t } from \"@tailor-platform/sdk\";\n import { getExtendedTable } from \"@tailor-platform/sdk/plugin\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n import { ${exportName} as sourceTable } from ${JSON.stringify(typeImportPath)};\n\n const configPath = join(import.meta.dirname, ${JSON.stringify(configImportPath)});\n const table = await getExtendedTable(configPath, sourceTable);\n\n ${generateSchemaTypeCode(\"table\", optionalFields, omitFields)}\n\n export const hook = createTailorDBHook(table);\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, table),${schemaOptionsCode}\n );\n\n `;\n}\n\n/**\n * Parameters for generating a plugin-generated table's schema file\n */\nexport interface PluginSchemaParams {\n /** Relative path from schema output to tailor.config.ts */\n configImportPath: string;\n /** Relative import path to the original table file (for table-attached plugins) */\n originalImportPath?: string;\n}\n\n/**\n * Generates the schema file content using getGeneratedTable API\n * (for plugin-generated tables)\n * @param metadata - lines-db metadata (must have pluginSource)\n * @param params - Plugin import paths\n * @returns Schema file contents\n */\nexport function generateLinesDbSchemaFileWithPluginAPI(\n metadata: LinesDbMetadata,\n params: PluginSchemaParams,\n): string {\n const { tableName, exportName, optionalFields, omitFields, foreignKeys, indexes, pluginSource } =\n metadata;\n\n if (!pluginSource) {\n throw new Error(`pluginSource is required for plugin-generated table \"${tableName}\"`);\n }\n\n const { configImportPath, originalImportPath } = params;\n\n const schemaTypeCode = ml /* ts */ `\n const schemaType = t.object({\n ...${exportName}.pickFields(${JSON.stringify(optionalFields)}, { optional: true }),\n ...${exportName}.omitFields(${JSON.stringify([...optionalFields, ...omitFields])}),\n });\n `;\n\n const schemaOptionsCode = generateSchemaOptions(foreignKeys, indexes);\n\n // Table-attached plugin (e.g., changeset): import the original table and use getGeneratedTable(configPath, pluginId, table, kind)\n if (pluginSource.originalExportName && originalImportPath && pluginSource.generatedTableKind) {\n return ml /* ts */ `\n import { join } from \"node:path\";\n import { t } from \"@tailor-platform/sdk\";\n import { getGeneratedTable } from \"@tailor-platform/sdk/plugin\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n import { ${pluginSource.originalExportName} } from \"${originalImportPath}\";\n\n const configPath = join(import.meta.dirname, \"${configImportPath}\");\n const ${exportName} = await getGeneratedTable(configPath, \"${pluginSource.pluginId}\", ${pluginSource.originalExportName}, \"${pluginSource.generatedTableKind}\");\n\n ${schemaTypeCode}\n\n export const hook = createTailorDBHook(${exportName});\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, ${exportName}),${schemaOptionsCode}\n );\n\n `;\n }\n\n // Namespace plugin (e.g., audit-log): use getGeneratedTable(configPath, pluginId, null, kind)\n // For namespace plugins, generatedTableKind is required\n if (!pluginSource.generatedTableKind) {\n throw new Error(\n `Namespace plugin \"${pluginSource.pluginId}\" must provide generatedTableKind for table \"${tableName}\"`,\n );\n }\n\n return ml /* ts */ `\n import { join } from \"node:path\";\n import { t } from \"@tailor-platform/sdk\";\n import { getGeneratedTable } from \"@tailor-platform/sdk/plugin\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n\n const configPath = join(import.meta.dirname, \"${configImportPath}\");\n const ${exportName} = await getGeneratedTable(configPath, \"${pluginSource.pluginId}\", null, \"${pluginSource.generatedTableKind}\");\n\n ${schemaTypeCode}\n\n export const hook = createTailorDBHook(${exportName});\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, ${exportName}),${schemaOptionsCode}\n );\n\n `;\n}\n","import * as path from \"pathe\";\nimport { assertDefined } from \"#/utils/assert\";\nimport { processIdpUser, generateIdpUserSchemaFile } from \"./idp-user-processor\";\nimport {\n processLinesDb,\n generateLinesDbSchemaFile,\n generateLinesDbSchemaFileWithPluginAPI,\n type PluginSchemaParams,\n} from \"./lines-db-processor\";\nimport type { Plugin, GeneratorResult, TailorDBReadyContext } from \"#/plugin/types\";\n\n/** Unique identifier for the seed generator plugin. */\nexport const SeedGeneratorID = \"@tailor-platform/seed\";\n\ntype DisableIdpUserSyncDirections = {\n /**\n * Skip emitting the foreign key from `<userProfile>.<usernameField>` to\n * `_User.name`. Defaults to `false` (FK emitted).\n *\n * Set to `true` to seed pre-registration states such as\n * invited-but-not-registered users.\n */\n userToIdp?: boolean;\n /**\n * Skip emitting the foreign key from `_User.name` to\n * `<userProfile>.<usernameField>`. Defaults to `false` (FK emitted).\n *\n * Set to `true` to seed `_User` rows that do not yet have a corresponding\n * userProfile row.\n */\n idpToUser?: boolean;\n};\n\nexport type SeedPluginOptions = {\n distPath: string;\n machineUserName?: string;\n /**\n * Disable individual `_User <-> userProfile` foreign keys emitted into\n * the generated seed schema. Both directions are emitted by default.\n *\n * Set a direction to `true` to relax it — for example to seed invited\n * users that do not yet have an IdP credential.\n */\n disableIdpUserSync?: DisableIdpUserSyncDirections;\n /**\n * Named combinations of datasets, so `tailor seed apply --dataset preview`\n * stands for every dataset directory the name lists. A dataset directory is\n * a directory under `<distPath>/data/` holding JSONL files for some of the\n * tables; its rows are applied on top of the rows in `<distPath>/data/`.\n * @example { preview: [\"cogs\", \"shipments\"], dev: [\"local\"] }\n */\n datasets?: Record<string, readonly string[]>;\n};\n\ndeclare module \"@tailor-platform/sdk/plugin\" {\n interface PluginConfigRegistry {\n \"@tailor-platform/seed\": SeedPluginOptions;\n }\n}\n\nfunction resolveIdpUserSyncFKs(option: SeedPluginOptions[\"disableIdpUserSync\"]): {\n emitUserToIdpFK: boolean;\n emitIdpToUserFK: boolean;\n} {\n return {\n emitUserToIdpFK: !(option?.userToIdp ?? false),\n emitIdpToUserFK: !(option?.idpToUser ?? false),\n };\n}\n/**\n * Plugin that generates seed data and schema files consumed by the\n * `tailor seed` commands (@tailor-platform/sdk-plugin-seed).\n * @param options - Plugin options\n * @param options.distPath - Output directory path for generated seed files\n * @param options.machineUserName - Default machine user name for authentication\n * @param options.disableIdpUserSync - Skip emitting individual `_User <-> userProfile` foreign keys. Both directions are emitted by default; set a direction to `true` to relax that side.\n * @param options.datasets - Named combinations of dataset directories under `<distPath>/data/`, selected with `tailor seed apply --dataset <name>`.\n * @returns Plugin instance with onTailorDBReady hook\n */\nexport function seedPlugin(options: SeedPluginOptions): Plugin<unknown, SeedPluginOptions> {\n return {\n id: SeedGeneratorID,\n description: \"Generates seed data and schema files for the tailor seed CLI plugin\",\n pluginConfig: options,\n\n async onTailorDBReady(ctx: TailorDBReadyContext<SeedPluginOptions>): Promise<GeneratorResult> {\n const files: GeneratorResult[\"files\"] = [];\n\n // Process IdP user early so we can add reverse FK to the user profile type\n const idpUser = ctx.auth ? (processIdpUser(ctx.auth) ?? null) : null;\n const idpUserSyncFKs = resolveIdpUserSyncFKs(ctx.pluginConfig.disableIdpUserSync);\n\n for (const ns of ctx.tailordb) {\n for (const [tableName, type] of Object.entries(ns.tables)) {\n const source = assertDefined(\n ns.sourceInfo.get(tableName),\n `source info missing for table: ${tableName}`,\n );\n const linesDb = processLinesDb(type, source);\n\n // Add reverse FK from userProfile table to _User (opt-out via disableIdpUserSync.userToIdp: true)\n if (\n idpUserSyncFKs.emitUserToIdpFK &&\n idpUser &&\n tableName === idpUser.schema.userTableName\n ) {\n linesDb.foreignKeys.push({\n column: idpUser.schema.usernameField,\n references: {\n table: \"_User\",\n column: \"name\",\n },\n });\n }\n\n // Generate empty JSONL data file\n files.push({\n path: path.join(ctx.pluginConfig.distPath, \"data\", `${linesDb.tableName}.jsonl`),\n content: \"\",\n skipIfExists: true,\n });\n\n const schemaOutputPath = path.join(\n ctx.pluginConfig.distPath,\n \"data\",\n `${linesDb.tableName}.schema.ts`,\n );\n\n // Plugin-generated table: use getGeneratedTable API\n if (linesDb.pluginSource && linesDb.pluginSource.pluginImportPath) {\n // Build original type import path\n let originalImportPath: string | undefined;\n if (linesDb.pluginSource.originalFilePath && linesDb.pluginSource.originalExportName) {\n const relativePath = path.relative(\n path.dirname(schemaOutputPath),\n linesDb.pluginSource.originalFilePath,\n );\n originalImportPath = relativePath.replace(/\\.ts$/, \"\").startsWith(\".\")\n ? relativePath.replace(/\\.ts$/, \"\")\n : `./${relativePath.replace(/\\.ts$/, \"\")}`;\n }\n\n // Compute relative path from schema output to config file\n const configImportPath = path.relative(path.dirname(schemaOutputPath), ctx.configPath);\n\n const params: PluginSchemaParams = {\n configImportPath,\n originalImportPath,\n };\n\n const schemaContent = generateLinesDbSchemaFileWithPluginAPI(linesDb, params);\n\n files.push({\n path: schemaOutputPath,\n content: schemaContent,\n });\n } else {\n // User-defined type: import from source file\n const relativePath = path.relative(path.dirname(schemaOutputPath), linesDb.importPath);\n const typeImportPath = relativePath.replace(/\\.ts$/, \"\").startsWith(\".\")\n ? relativePath.replace(/\\.ts$/, \"\")\n : `./${relativePath.replace(/\\.ts$/, \"\")}`;\n // A table with plugins attached may carry fields the source file does not\n // declare, so its schema loads the table through the config.\n const hasPlugins = (ns.pluginAttachments.get(tableName)?.length ?? 0) > 0;\n const configImportPath = hasPlugins\n ? path.relative(path.dirname(schemaOutputPath), ctx.configPath)\n : undefined;\n const schemaContent = generateLinesDbSchemaFile(linesDb, {\n typeImportPath,\n configImportPath,\n });\n\n files.push({\n path: schemaOutputPath,\n content: schemaContent,\n });\n }\n }\n }\n\n if (idpUser) {\n // Generate empty JSONL data file\n files.push({\n path: path.join(ctx.pluginConfig.distPath, idpUser.dataFile),\n content: \"\",\n skipIfExists: true,\n });\n\n // Generate schema file with foreign key (opt-out via disableIdpUserSync.idpToUser: true)\n files.push({\n path: path.join(ctx.pluginConfig.distPath, \"data\", `${idpUser.name}.schema.ts`),\n content: generateIdpUserSchemaFile({\n usernameField: idpUser.schema.usernameField,\n userTableName: idpUser.schema.userTableName,\n includeUserProfileFK: idpUserSyncFKs.emitIdpToUserFK,\n }),\n });\n }\n\n return { files };\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"seed-C3P_T_Eh.mjs","names":["assertDefined","isPluginGeneratedTable","path","auth","tableName","idpNamespace","listIdpUsersFunction","options","schemaBody","includeUserProfileFK","userTableName","usernameField","source","type","field","optionalFields","omitFields","indexes","foreignKeys","fieldName","indexName","indexDef","schemaOptions","fk","index","option","tableVariable","metadata","params","schemaOptionsCode","configImportPath","exportName","typeImportPath","pluginSource","schemaTypeCode","originalImportPath","SeedGeneratorID","files","idpUser","ctx","idpUserSyncFKs","ns","linesDb","schemaOutputPath","relativePath","schemaContent"],"sources":["../src/plugin/builtin/seed/idp-user-processor.ts","../src/plugin/builtin/seed/lines-db-processor.ts","../src/plugin/builtin/seed/index.ts"],"sourcesContent":["import ml from \"#/utils/multiline\";\nimport type { GeneratorAuthInput } from \"#/plugin/types\";\n\nexport interface IdpUserMetadata {\n name: \"_User\";\n dependencies: string[];\n dataFile: string;\n idpNamespace: string;\n schema: {\n usernameField: string;\n userTableName: string;\n };\n}\n\n/**\n * Processes auth configuration to generate IdP user seed metadata\n * @param auth - Auth configuration from generator\n * @returns IdP user metadata or undefined if not applicable\n */\nexport function processIdpUser(auth: GeneratorAuthInput): IdpUserMetadata | undefined {\n // Only process if idProvider is BuiltInIdP and userProfile is defined\n if (auth.idProvider?.kind !== \"BuiltInIdP\" || !auth.userProfile) {\n return undefined;\n }\n\n const { tableName, usernameField } = auth.userProfile;\n\n return {\n name: \"_User\",\n dependencies: [tableName],\n dataFile: \"data/_User.jsonl\",\n idpNamespace: auth.idProvider.namespace,\n schema: {\n usernameField,\n userTableName: tableName,\n },\n };\n}\n\n/**\n * Generates the server-side IDP seed script code.\n * Uses the global tailor.idp.Client - no bundling required.\n * @param idpNamespace - The IDP namespace name\n * @returns Script code string\n */\nexport function generateIdpSeedScriptCode(idpNamespace: string): string {\n return ml /* ts */ `\n export async function main(input) {\n const client = new tailor.idp.Client({ namespace: \"${idpNamespace}\" });\n const errors = [];\n let processed = 0;\n let created = 0;\n let updated = 0;\n let skipped = 0;\n const upsert = input.upsert === true;\n // Rows may arrive as one chunk of a larger dataset; report row numbers\n // relative to the whole dataset so they match the source JSONL.\n const offset = typeof input.offset === \"number\" ? input.offset : 0;\n const total = typeof input.total === \"number\" ? input.total : input.users.length;\n\n for (let i = 0; i < input.users.length; i++) {\n try {\n if (upsert) {\n let existing;\n let lookupError;\n try {\n existing = await client.userByName(input.users[i].name);\n } catch (error) {\n existing = undefined;\n lookupError = error instanceof Error ? error.message : String(error);\n }\n\n if (existing) {\n const { name, ...attributes } = input.users[i];\n if (Object.keys(attributes).length === 0) {\n skipped++;\n } else {\n await client.updateUser({ id: existing.id, ...attributes });\n updated++;\n }\n } else {\n try {\n await client.createUser(input.users[i]);\n created++;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n lookupError ? \\`create failed (\\${message}); lookup failed (\\${lookupError})\\` : message,\n );\n }\n }\n } else {\n await client.createUser(input.users[i]);\n created++;\n }\n processed++;\n console.log(\\`[_User] \\${offset + i + 1}/\\${total}: \\${input.users[i].name}\\`);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n errors.push(\\`Row \\${offset + i + 1} (\\${input.users[i].name}): \\${message}\\`);\n console.error(\\`[_User] Row \\${offset + i + 1} failed: \\${message}\\`);\n }\n }\n\n return {\n success: errors.length === 0,\n processed,\n created,\n updated,\n skipped,\n errors,\n };\n }\n `;\n}\n\nconst listIdpUsersFunction = ml /* ts */ `\n async function listUsers(client) {\n let after = undefined;\n const users = [];\n do {\n const response = await client.users(after ? { after } : undefined);\n for (const user of response.users || []) {\n users.push({ id: user.id, name: user.name });\n }\n after = response.nextPageToken;\n } while (after);\n console.log(\\`Found \\${users.length} IDP users to delete\\`);\n return users;\n }\n`;\n\n/**\n * Generates the server-side script that lists every IdP user for truncation.\n * @param idpNamespace - The IDP namespace name\n * @returns Script code string\n */\nexport function generateIdpListUsersScriptCode(idpNamespace: string): string {\n return ml /* ts */ `\n ${listIdpUsersFunction}\n\n export async function main() {\n const client = new tailor.idp.Client({ namespace: \"${idpNamespace}\" });\n const users = await listUsers(client);\n return { success: true, users };\n }\n `;\n}\n\n/**\n * Generates the server-side IDP truncation script code.\n * Deletes the users passed in `input.users` (one chunk of the listing produced by\n * {@link generateIdpListUsersScriptCode}), or every user when no chunk is passed so\n * older seed plugins that call it without input keep working. A user that is already\n * gone counts as deleted.\n * @param idpNamespace - The IDP namespace name\n * @returns Script code string\n */\nexport function generateIdpTruncateScriptCode(idpNamespace: string): string {\n return ml /* ts */ `\n ${listIdpUsersFunction}\n\n export async function main(input) {\n const client = new tailor.idp.Client({ namespace: \"${idpNamespace}\" });\n const errors = [];\n let deleted = 0;\n let notFound = 0;\n const users = Array.isArray(input.users) ? input.users : await listUsers(client);\n const offset = typeof input.offset === \"number\" ? input.offset : 0;\n const total = typeof input.total === \"number\" ? input.total : users.length;\n\n for (let i = 0; i < users.length; i++) {\n const user = users[i];\n try {\n await client.deleteUser(user.id);\n deleted++;\n console.log(\\`[_User] Deleted \\${offset + i + 1}/\\${total}: \\${user.name}\\`);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (/user not found/i.test(message)) {\n notFound++;\n console.log(\\`[_User] Already deleted \\${offset + i + 1}/\\${total}: \\${user.name}\\`);\n continue;\n }\n errors.push(\\`User \\${user.id} (\\${user.name}): \\${message}\\`);\n console.error(\\`[_User] Delete failed for \\${user.name}: \\${message}\\`);\n }\n }\n\n return {\n success: errors.length === 0,\n deleted,\n notFound,\n total: users.length,\n errors,\n };\n }\n `;\n}\n\ntype GenerateIdpUserSchemaFileOptions = {\n usernameField: string;\n userTableName: string;\n /**\n * When `true` (default), emit a foreign key from `_User.name` to the\n * userProfile table's username field so that seed validation rejects `_User`\n * rows without a matching userProfile row. Set to `false` to seed `_User`\n * rows that do not yet have a corresponding userProfile row.\n */\n includeUserProfileFK?: boolean;\n};\n\n/**\n * Generates the schema file content for IdP users. Emits the\n * `_User.name -> <userProfile>.<usernameField>` foreign key by default; pass\n * `includeUserProfileFK: false` to omit it (e.g. when seeding `_User` rows\n * that do not yet have a corresponding userProfile row).\n * @param options - Schema generation options\n * @param options.usernameField - Username field name\n * @param options.userTableName - TailorDB user table name\n * @param options.includeUserProfileFK - Whether to emit the `_User -> userProfile` foreign key (default `true`)\n * @returns Schema file contents\n */\nexport function generateIdpUserSchemaFile(options: GenerateIdpUserSchemaFileOptions): string {\n const { usernameField, userTableName, includeUserProfileFK = true } = options;\n const schemaBody = includeUserProfileFK\n ? ml`\n primaryKey: \"name\",\n indexes: [\n { name: \"_user_name_unique_idx\", columns: [\"name\"], unique: true },\n ],\n foreignKeys: [\n {\n column: \"name\",\n references: {\n table: \"${userTableName}\",\n column: \"${usernameField}\",\n },\n },\n ],\n `\n : ml`\n primaryKey: \"name\",\n indexes: [\n { name: \"_user_name_unique_idx\", columns: [\"name\"], unique: true },\n ],\n `;\n\n return ml /* ts */ `\n import { t } from \"@tailor-platform/sdk\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createStandardSchema } from \"@tailor-platform/sdk/test\";\n\n const schemaType = t.object({\n name: t.string(),\n password: t.string(),\n });\n\n // Simple identity hook for _User (no TailorDB backing table)\n export const hook = <T>(data: unknown) => data as T;\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook),\n {\n ${schemaBody}\n }\n );\n\n `;\n}\n","import { isPluginGeneratedTable } from \"#/parser/service/tailordb/type-source\";\nimport ml from \"#/utils/multiline\";\nimport type {\n ParsedField,\n PluginGeneratedTableSource,\n TailorDBType,\n TypeSourceInfoEntry,\n} from \"#/parser/service/tailordb/types\";\nimport type { LinesDbMetadata } from \"./types\";\nimport type { ForeignKeyDefinition, IndexDefinition } from \"lines-db\";\n\n/**\n * Processes TailorDB tables to generate lines-db metadata\n * @param type - Parsed TailorDB table\n * @param source - Source file info\n * @returns Generated lines-db metadata\n */\nexport function processLinesDb(type: TailorDBType, source: TypeSourceInfoEntry): LinesDbMetadata {\n if (isPluginGeneratedTable(source)) {\n // Plugin-generated table\n return processLinesDbForPluginTable(type, source);\n }\n\n // User-defined table\n if (!source.filePath) {\n throw new Error(`Missing source info for table ${type.name}`);\n }\n if (!source.exportName) {\n throw new Error(`Missing export name for table ${type.name}`);\n }\n\n const { optionalFields, omitFields, indexes, foreignKeys } = extractFieldMetadata(type);\n\n return {\n tableName: type.name,\n exportName: source.exportName,\n importPath: source.filePath,\n optionalFields,\n omitFields,\n foreignKeys,\n indexes,\n };\n}\n\n/**\n * Process lines-db metadata for plugin-generated tables\n * @param type - Parsed TailorDB table\n * @param source - Plugin-generated table source info\n * @returns Generated lines-db metadata with plugin source\n */\nfunction processLinesDbForPluginTable(\n type: TailorDBType,\n source: PluginGeneratedTableSource,\n): LinesDbMetadata {\n const { optionalFields, omitFields, indexes, foreignKeys } = extractFieldMetadata(type);\n\n return {\n tableName: type.name,\n exportName: source.exportName,\n importPath: \"\",\n optionalFields,\n omitFields,\n foreignKeys,\n indexes,\n pluginSource: source,\n };\n}\n\n/**\n * Whether the platform produces a value for the field on create when a row omits it,\n * either from a create hook or from a schema default. Such fields stay optional in seed\n * data even when the table marks them required.\n * @param field - Parsed TailorDB field\n * @returns True when a seed row does not have to supply the field\n */\nfunction isGeneratedOnCreate(field: ParsedField): boolean {\n return field.config.hooks?.create !== undefined || field.config.default !== undefined;\n}\n\n/**\n * Extract field metadata from TailorDB table\n * @param type - Parsed TailorDB table\n * @returns Field metadata including optional fields, omit fields, indexes, and foreign keys\n */\nfunction extractFieldMetadata(type: TailorDBType): {\n optionalFields: string[];\n omitFields: string[];\n indexes: IndexDefinition[];\n foreignKeys: ForeignKeyDefinition[];\n} {\n const optionalFields = [\"id\"]; // id is always optional\n const omitFields: string[] = [];\n const indexes: IndexDefinition[] = [];\n const foreignKeys: ForeignKeyDefinition[] = [];\n\n // Find fields generated on create, or serial\n for (const [fieldName, field] of Object.entries(type.fields)) {\n if (isGeneratedOnCreate(field)) {\n optionalFields.push(fieldName);\n }\n // Serial fields are auto-generated, so they are excluded from the seed schema entirely\n if (field.config.serial) {\n omitFields.push(fieldName);\n }\n if (field.config.unique) {\n indexes.push({\n name: `${type.name.toLowerCase()}_${fieldName}_unique_idx`,\n columns: [fieldName],\n unique: true,\n });\n }\n }\n\n // Extract indexes\n if (type.indexes) {\n for (const [indexName, indexDef] of Object.entries(type.indexes)) {\n indexes.push({\n name: indexName,\n columns: indexDef.fields,\n unique: indexDef.unique,\n });\n }\n }\n\n // Extract foreign keys from relations\n for (const [fieldName, field] of Object.entries(type.fields)) {\n if (field.relation) {\n foreignKeys.push({\n column: fieldName,\n references: {\n table: field.relation.targetType,\n column: field.relation.key,\n },\n });\n }\n }\n\n return { optionalFields, omitFields, indexes, foreignKeys };\n}\n\n/**\n * Generate schema options code for lines-db\n * @param foreignKeys - Foreign key definitions\n * @param indexes - Index definitions\n * @returns Schema options code string\n */\nfunction generateSchemaOptions(\n foreignKeys: ForeignKeyDefinition[],\n indexes: IndexDefinition[],\n): string {\n const schemaOptions: string[] = [];\n\n if (foreignKeys.length > 0) {\n schemaOptions.push(`foreignKeys: [`);\n foreignKeys.forEach((fk) => {\n schemaOptions.push(` ${JSON.stringify(fk)},`);\n });\n schemaOptions.push(`],`);\n }\n\n if (indexes.length > 0) {\n schemaOptions.push(`indexes: [`);\n indexes.forEach((index) => {\n schemaOptions.push(` ${JSON.stringify(index)},`);\n });\n schemaOptions.push(\"],\");\n }\n\n return schemaOptions.length > 0\n ? [\"\\n {\", ...schemaOptions.map((option) => ` ${option}`), \" }\"].join(\"\\n\")\n : \"\";\n}\n\n/**\n * Parameters for generating a user-defined table's schema file\n */\nexport interface UserTableSchemaParams {\n /** Relative import path to the table's source file */\n typeImportPath: string;\n /**\n * Relative path from the schema output to tailor.config.ts. Set when plugins are\n * attached to the table: the schema then loads the table through `getExtendedTable`\n * so the fields those plugins add are part of the seed schema.\n */\n configImportPath?: string;\n}\n\nfunction generateSchemaTypeCode(\n tableVariable: string,\n optionalFields: string[],\n omitFields: string[],\n): string {\n return ml /* ts */ `\n const schemaType = t.object({\n ...${tableVariable}.pickFields(${JSON.stringify(optionalFields)}, { optional: true }),\n ...${tableVariable}.omitFields(${JSON.stringify([...optionalFields, ...omitFields])}),\n });\n `;\n}\n\n/**\n * Generates the schema file content for lines-db (for user-defined tables with import)\n * @param metadata - lines-db metadata\n * @param params - Import paths for the table and, when plugins are attached, the config\n * @returns Schema file contents\n */\nexport function generateLinesDbSchemaFile(\n metadata: LinesDbMetadata,\n params: UserTableSchemaParams,\n): string {\n const { exportName, optionalFields, omitFields, foreignKeys, indexes } = metadata;\n const { typeImportPath, configImportPath } = params;\n\n const schemaOptionsCode = generateSchemaOptions(foreignKeys, indexes);\n\n if (configImportPath === undefined) {\n return ml /* ts */ `\n import { t } from \"@tailor-platform/sdk\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n import { ${exportName} } from \"${typeImportPath}\";\n\n ${generateSchemaTypeCode(exportName, optionalFields, omitFields)}\n\n export const hook = createTailorDBHook(${exportName});\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, ${exportName}),${schemaOptionsCode}\n );\n\n `;\n }\n\n // The source file exports the table without the fields its plugins add, so the\n // schema is built from the table `getExtendedTable` returns instead.\n return ml /* ts */ `\n import { join } from \"node:path\";\n import { t } from \"@tailor-platform/sdk\";\n import { getExtendedTable } from \"@tailor-platform/sdk/plugin\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n import { ${exportName} as sourceTable } from ${JSON.stringify(typeImportPath)};\n\n const configPath = join(import.meta.dirname, ${JSON.stringify(configImportPath)});\n const table = await getExtendedTable(configPath, sourceTable);\n\n ${generateSchemaTypeCode(\"table\", optionalFields, omitFields)}\n\n export const hook = createTailorDBHook(table);\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, table),${schemaOptionsCode}\n );\n\n `;\n}\n\n/**\n * Parameters for generating a plugin-generated table's schema file\n */\nexport interface PluginSchemaParams {\n /** Relative path from schema output to tailor.config.ts */\n configImportPath: string;\n /** Relative import path to the original table file (for table-attached plugins) */\n originalImportPath?: string;\n}\n\n/**\n * Generates the schema file content using getGeneratedTable API\n * (for plugin-generated tables)\n * @param metadata - lines-db metadata (must have pluginSource)\n * @param params - Plugin import paths\n * @returns Schema file contents\n */\nexport function generateLinesDbSchemaFileWithPluginAPI(\n metadata: LinesDbMetadata,\n params: PluginSchemaParams,\n): string {\n const { tableName, exportName, optionalFields, omitFields, foreignKeys, indexes, pluginSource } =\n metadata;\n\n if (!pluginSource) {\n throw new Error(`pluginSource is required for plugin-generated table \"${tableName}\"`);\n }\n\n const { configImportPath, originalImportPath } = params;\n\n const schemaTypeCode = ml /* ts */ `\n const schemaType = t.object({\n ...${exportName}.pickFields(${JSON.stringify(optionalFields)}, { optional: true }),\n ...${exportName}.omitFields(${JSON.stringify([...optionalFields, ...omitFields])}),\n });\n `;\n\n const schemaOptionsCode = generateSchemaOptions(foreignKeys, indexes);\n\n // Table-attached plugin (e.g., changeset): import the original table and use getGeneratedTable(configPath, pluginId, table, kind)\n if (pluginSource.originalExportName && originalImportPath && pluginSource.generatedTableKind) {\n return ml /* ts */ `\n import { join } from \"node:path\";\n import { t } from \"@tailor-platform/sdk\";\n import { getGeneratedTable } from \"@tailor-platform/sdk/plugin\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n import { ${pluginSource.originalExportName} } from \"${originalImportPath}\";\n\n const configPath = join(import.meta.dirname, \"${configImportPath}\");\n const ${exportName} = await getGeneratedTable(configPath, \"${pluginSource.pluginId}\", ${pluginSource.originalExportName}, \"${pluginSource.generatedTableKind}\");\n\n ${schemaTypeCode}\n\n export const hook = createTailorDBHook(${exportName});\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, ${exportName}),${schemaOptionsCode}\n );\n\n `;\n }\n\n // Namespace plugin (e.g., audit-log): use getGeneratedTable(configPath, pluginId, null, kind)\n // For namespace plugins, generatedTableKind is required\n if (!pluginSource.generatedTableKind) {\n throw new Error(\n `Namespace plugin \"${pluginSource.pluginId}\" must provide generatedTableKind for table \"${tableName}\"`,\n );\n }\n\n return ml /* ts */ `\n import { join } from \"node:path\";\n import { t } from \"@tailor-platform/sdk\";\n import { getGeneratedTable } from \"@tailor-platform/sdk/plugin\";\n import { defineSchema } from \"@tailor-platform/sdk/seed\";\n import { createTailorDBHook, createStandardSchema } from \"@tailor-platform/sdk/test\";\n\n const configPath = join(import.meta.dirname, \"${configImportPath}\");\n const ${exportName} = await getGeneratedTable(configPath, \"${pluginSource.pluginId}\", null, \"${pluginSource.generatedTableKind}\");\n\n ${schemaTypeCode}\n\n export const hook = createTailorDBHook(${exportName});\n\n export const schema = defineSchema(\n createStandardSchema(schemaType, hook, ${exportName}),${schemaOptionsCode}\n );\n\n `;\n}\n","import * as path from \"pathe\";\nimport { assertDefined } from \"#/utils/assert\";\nimport { processIdpUser, generateIdpUserSchemaFile } from \"./idp-user-processor\";\nimport {\n processLinesDb,\n generateLinesDbSchemaFile,\n generateLinesDbSchemaFileWithPluginAPI,\n type PluginSchemaParams,\n} from \"./lines-db-processor\";\nimport type { Plugin, GeneratorResult, TailorDBReadyContext } from \"#/plugin/types\";\n\n/** Unique identifier for the seed generator plugin. */\nexport const SeedGeneratorID = \"@tailor-platform/seed\";\n\ntype DisableIdpUserSyncDirections = {\n /**\n * Skip emitting the foreign key from `<userProfile>.<usernameField>` to\n * `_User.name`. Defaults to `false` (FK emitted).\n *\n * Set to `true` to seed pre-registration states such as\n * invited-but-not-registered users.\n */\n userToIdp?: boolean;\n /**\n * Skip emitting the foreign key from `_User.name` to\n * `<userProfile>.<usernameField>`. Defaults to `false` (FK emitted).\n *\n * Set to `true` to seed `_User` rows that do not yet have a corresponding\n * userProfile row.\n */\n idpToUser?: boolean;\n};\n\nexport type SeedPluginOptions = {\n distPath: string;\n machineUserName?: string;\n /**\n * Disable individual `_User <-> userProfile` foreign keys emitted into\n * the generated seed schema. Both directions are emitted by default.\n *\n * Set a direction to `true` to relax it — for example to seed invited\n * users that do not yet have an IdP credential.\n */\n disableIdpUserSync?: DisableIdpUserSyncDirections;\n /**\n * Named combinations of datasets, so `tailor seed apply --dataset preview`\n * stands for every dataset directory the name lists. A dataset directory is\n * a directory under `<distPath>/data/` holding JSONL files for some of the\n * tables; its rows are applied on top of the rows in `<distPath>/data/`.\n * @example { preview: [\"cogs\", \"shipments\"], dev: [\"local\"] }\n */\n datasets?: Record<string, readonly string[]>;\n};\n\ndeclare module \"@tailor-platform/sdk/plugin\" {\n interface PluginConfigRegistry {\n \"@tailor-platform/seed\": SeedPluginOptions;\n }\n}\n\nfunction resolveIdpUserSyncFKs(option: SeedPluginOptions[\"disableIdpUserSync\"]): {\n emitUserToIdpFK: boolean;\n emitIdpToUserFK: boolean;\n} {\n return {\n emitUserToIdpFK: !(option?.userToIdp ?? false),\n emitIdpToUserFK: !(option?.idpToUser ?? false),\n };\n}\n/**\n * Plugin that generates seed data and schema files consumed by the\n * `tailor seed` commands (@tailor-platform/sdk-plugin-seed).\n * @param options - Plugin options\n * @param options.distPath - Output directory path for generated seed files\n * @param options.machineUserName - Default machine user name for authentication\n * @param options.disableIdpUserSync - Skip emitting individual `_User <-> userProfile` foreign keys. Both directions are emitted by default; set a direction to `true` to relax that side.\n * @param options.datasets - Named combinations of dataset directories under `<distPath>/data/`, selected with `tailor seed apply --dataset <name>`.\n * @returns Plugin instance with onTailorDBReady hook\n */\nexport function seedPlugin(options: SeedPluginOptions): Plugin<unknown, SeedPluginOptions> {\n return {\n id: SeedGeneratorID,\n description: \"Generates seed data and schema files for the tailor seed CLI plugin\",\n pluginConfig: options,\n\n async onTailorDBReady(ctx: TailorDBReadyContext<SeedPluginOptions>): Promise<GeneratorResult> {\n const files: GeneratorResult[\"files\"] = [];\n\n // Process IdP user early so we can add reverse FK to the user profile type\n const idpUser = ctx.auth ? (processIdpUser(ctx.auth) ?? null) : null;\n const idpUserSyncFKs = resolveIdpUserSyncFKs(ctx.pluginConfig.disableIdpUserSync);\n\n for (const ns of ctx.tailordb) {\n for (const [tableName, type] of Object.entries(ns.tables)) {\n const source = assertDefined(\n ns.sourceInfo.get(tableName),\n `source info missing for table: ${tableName}`,\n );\n const linesDb = processLinesDb(type, source);\n\n // Add reverse FK from userProfile table to _User (opt-out via disableIdpUserSync.userToIdp: true)\n if (\n idpUserSyncFKs.emitUserToIdpFK &&\n idpUser &&\n tableName === idpUser.schema.userTableName\n ) {\n linesDb.foreignKeys.push({\n column: idpUser.schema.usernameField,\n references: {\n table: \"_User\",\n column: \"name\",\n },\n });\n }\n\n // Generate empty JSONL data file\n files.push({\n path: path.join(ctx.pluginConfig.distPath, \"data\", `${linesDb.tableName}.jsonl`),\n content: \"\",\n skipIfExists: true,\n });\n\n const schemaOutputPath = path.join(\n ctx.pluginConfig.distPath,\n \"data\",\n `${linesDb.tableName}.schema.ts`,\n );\n\n // Plugin-generated table: use getGeneratedTable API\n if (linesDb.pluginSource && linesDb.pluginSource.pluginImportPath) {\n // Build original type import path\n let originalImportPath: string | undefined;\n if (linesDb.pluginSource.originalFilePath && linesDb.pluginSource.originalExportName) {\n const relativePath = path.relative(\n path.dirname(schemaOutputPath),\n linesDb.pluginSource.originalFilePath,\n );\n originalImportPath = relativePath.replace(/\\.ts$/, \"\").startsWith(\".\")\n ? relativePath.replace(/\\.ts$/, \"\")\n : `./${relativePath.replace(/\\.ts$/, \"\")}`;\n }\n\n // Compute relative path from schema output to config file\n const configImportPath = path.relative(path.dirname(schemaOutputPath), ctx.configPath);\n\n const params: PluginSchemaParams = {\n configImportPath,\n originalImportPath,\n };\n\n const schemaContent = generateLinesDbSchemaFileWithPluginAPI(linesDb, params);\n\n files.push({\n path: schemaOutputPath,\n content: schemaContent,\n });\n } else {\n // User-defined type: import from source file\n const relativePath = path.relative(path.dirname(schemaOutputPath), linesDb.importPath);\n const typeImportPath = relativePath.replace(/\\.ts$/, \"\").startsWith(\".\")\n ? relativePath.replace(/\\.ts$/, \"\")\n : `./${relativePath.replace(/\\.ts$/, \"\")}`;\n // A table with plugins attached may carry fields the source file does not\n // declare, so its schema loads the table through the config.\n const hasPlugins = (ns.pluginAttachments.get(tableName)?.length ?? 0) > 0;\n const configImportPath = hasPlugins\n ? path.relative(path.dirname(schemaOutputPath), ctx.configPath)\n : undefined;\n const schemaContent = generateLinesDbSchemaFile(linesDb, {\n typeImportPath,\n configImportPath,\n });\n\n files.push({\n path: schemaOutputPath,\n content: schemaContent,\n });\n }\n }\n }\n\n if (idpUser) {\n // Generate empty JSONL data file\n files.push({\n path: path.join(ctx.pluginConfig.distPath, idpUser.dataFile),\n content: \"\",\n skipIfExists: true,\n });\n\n // Generate schema file with foreign key (opt-out via disableIdpUserSync.idpToUser: true)\n files.push({\n path: path.join(ctx.pluginConfig.distPath, \"data\", `${idpUser.name}.schema.ts`),\n content: generateIdpUserSchemaFile({\n usernameField: idpUser.schema.usernameField,\n userTableName: idpUser.schema.userTableName,\n includeUserProfileFK: idpUserSyncFKs.emitIdpToUserFK,\n }),\n });\n }\n\n return { files };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -46,9 +46,9 @@ export declare function mockFile(options?: MockFileOptions): {
|
|
|
46
46
|
calls: FileCall[];
|
|
47
47
|
clear(): void;
|
|
48
48
|
reset(): void;
|
|
49
|
-
delete: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<void>>;
|
|
50
49
|
download: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadResponse>>;
|
|
51
50
|
downloadAsBase64: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadAsBase64Response>>;
|
|
51
|
+
delete: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<void>>;
|
|
52
52
|
getMetadata: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileMetadata>>;
|
|
53
53
|
downloadStream: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadStreamResponse>>;
|
|
54
54
|
uploadStream: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string, readableStream: ReadableStream<Uint8Array | ArrayBuffer>, options?: FileUploadStreamOptions) => Promise<FileUploadResponse>>;
|
package/docs/cli/application.md
CHANGED
|
@@ -174,6 +174,8 @@ tailor deploy --config apps/buyer/tailor.config.ts,apps/supplier/tailor.config.t
|
|
|
174
174
|
|
|
175
175
|
When multiple configs are provided, `deploy` creates or updates all configured services first, then updates the applications. This lets one application reference resources owned by another config with `external: true` during the same deploy.
|
|
176
176
|
|
|
177
|
+
Names that identify a resource across the whole workspace, such as application, executor, workflow, and static website names and service namespaces, must be unique across all configs in one deploy. Otherwise, `deploy` fails before planning any changes and lists every duplicate with the config files that define it.
|
|
178
|
+
|
|
177
179
|
Each config's `files` and `ignores` patterns (see [Service Configuration](../configuration.md#service-configuration)) resolve relative to that config's own directory, not the directory you ran `deploy` from. For example, `apps/buyer/tailor.config.ts` declaring `files: ["db/**/*.ts"]` loads files from `apps/buyer/db/`, independent of where `apps/supplier/tailor.config.ts`'s patterns resolve. If a config's relative patterns match nothing under its own directory, the SDK falls back to the invocation directory and logs a warning (see [Service Configuration](../configuration.md#service-configuration) for details).
|
|
178
180
|
|
|
179
181
|
**Migration Handling:**
|
|
@@ -253,10 +255,12 @@ Pass the global `--json` / `-j` flag to get machine-readable output.
|
|
|
253
255
|
}
|
|
254
256
|
],
|
|
255
257
|
"warnings": [
|
|
256
|
-
{ "type": "unmanaged", "resourceType": "
|
|
257
|
-
{ "type": "skippedSecret", "resourceType": "secret", "name": "DB_PASSWORD" }
|
|
258
|
+
{ "type": "unmanaged", "resourceType": "staticwebsite", "name": "legacy-site" },
|
|
259
|
+
{ "type": "skippedSecret", "resourceType": "secret.secret", "name": "my-vault/DB_PASSWORD" }
|
|
258
260
|
],
|
|
259
|
-
"conflicts": [
|
|
261
|
+
"conflicts": [
|
|
262
|
+
{ "resourceType": "tailordb.service", "name": "tailordb", "currentOwner": "other-app" }
|
|
263
|
+
]
|
|
260
264
|
}
|
|
261
265
|
```
|
|
262
266
|
|
|
@@ -264,6 +268,7 @@ Pass the global `--json` / `-j` flag to get machine-readable output.
|
|
|
264
268
|
- `changes` — planned resource changes, each with `action`, `name`, and optional `labels` / `namespace`. An update forced by the SDK version also has `forcedBySdkVersion: true`.
|
|
265
269
|
- `warnings` — resources not in config (`type: "unmanaged"`) or secrets with missing values (`type: "skippedSecret"`). Unmanaged resources require confirmation in apply mode (apply is cancelled if declined); skipped secrets are non-blocking.
|
|
266
270
|
- `conflicts` — resources owned by another application that conflict with the current config. Require confirmation in apply mode; apply is cancelled if declined.
|
|
271
|
+
- `resourceType` in `warnings` and `conflicts` is a stable identifier of the resource kind, such as `executor`, `tailordb.service`, or `secret.secret`. The human-readable plan shows a display name instead.
|
|
267
272
|
|
|
268
273
|
**Apply** (`--json`): writes a JSON object to stdout:
|
|
269
274
|
|
package/docs/cli/tailordb.md
CHANGED
|
@@ -98,6 +98,7 @@ tailor tailordb migration <command>
|
|
|
98
98
|
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
99
|
| [`tailordb migration generate`](#tailordb-migration-generate) | Generate migration files by detecting schema differences between current local tables and the previous migration snapshot. |
|
|
100
100
|
| [`tailordb migration rebaseline`](#tailordb-migration-rebaseline) | Collapse the full migration history into a new 0000 baseline. |
|
|
101
|
+
| [`tailordb migration resolve`](#tailordb-migration-resolve) | Settle a migration that a failed deploy left partially applied. |
|
|
101
102
|
| [`tailordb migration script`](#tailordb-migration-script) | Add a migration script (migrate.ts) template to an existing migration directory, or record with --no-script that a migration intentionally has none. |
|
|
102
103
|
| [`tailordb migration set`](#tailordb-migration-set) | Set migration checkpoint to a specific number. |
|
|
103
104
|
| [`tailordb migration status`](#tailordb-migration-status) | Show the current migration status for TailorDB namespaces, including applied and pending migrations. |
|
|
@@ -159,6 +160,39 @@ See [Global Options](../cli-reference.md#global-options) for options available t
|
|
|
159
160
|
|
|
160
161
|
Re-baselining removes migrations after 0000 from the working tree, records a new migration history ID, and resets the connected workspace checkpoint without changing its schema or data. Every environment must already have applied the latest migration before you run this command.
|
|
161
162
|
|
|
163
|
+
#### tailordb migration resolve
|
|
164
|
+
|
|
165
|
+
Settle a migration that a failed deploy left partially applied.
|
|
166
|
+
|
|
167
|
+
**Usage**
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
tailor tailordb migration resolve [options]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Options**
|
|
174
|
+
|
|
175
|
+
| Option | Alias | Description | Required | Default | Env |
|
|
176
|
+
| ------------------------------- | ----- | ---------------------------------------------------------------------- | -------- | -------------------- | ------------------------------ |
|
|
177
|
+
| `--workspace-id <WORKSPACE_ID>` | `-w` | Workspace ID | No | - | `TAILOR_PLATFORM_WORKSPACE_ID` |
|
|
178
|
+
| `--profile <PROFILE>` | `-p` | Workspace profile | No | - | `TAILOR_PLATFORM_PROFILE` |
|
|
179
|
+
| `--config <CONFIG>` | `-c` | Path to Tailor config file | No | `"tailor.config.ts"` | `TAILOR_CONFIG_PATH` |
|
|
180
|
+
| `--yes` | `-y` | Skip confirmation prompts | No | `false` | - |
|
|
181
|
+
| `--rollback` | - | Return the namespace to the schema before the in-progress migration | No | `false` | - |
|
|
182
|
+
| `--complete` | - | Apply the schema of the in-progress migration and mark it as completed | No | `false` | - |
|
|
183
|
+
| `--namespace <NAMESPACE>` | `-n` | Target TailorDB namespace (required if multiple namespaces exist) | No | - | - |
|
|
184
|
+
|
|
185
|
+
See [Global Options](../cli-reference.md#global-options) for options available to all commands.
|
|
186
|
+
|
|
187
|
+
**Notes**
|
|
188
|
+
|
|
189
|
+
Use this after a deploy failed partway through a migration whose `migrate.ts` exports `steps`. The failed migration stays in progress: when `maintenanceMode` is on in your config, the tables of its namespace stay in maintenance mode, and `sync` and `rebaseline` are refused, while `set` accepts only the number of the in-progress migration.
|
|
190
|
+
|
|
191
|
+
- `--rollback` sends the schema of the migration before the failed one. The next deploy runs the failed migration again from its first step. Data changes made by steps that already completed are not undone.
|
|
192
|
+
- `--complete` sends the schema of the failed migration itself and records it as completed, without running its `migrate.ts` again. Fix the data first (for example with `tailor query`), and make sure no record lacks a value for a field that becomes required.
|
|
193
|
+
|
|
194
|
+
Both options remove the in-progress record, take the tables out of maintenance mode, and remove the temporary resources of the failed run, so `deploy` and the other migration commands work again. As with `tailordb migration sync`, the local migration history must reproduce the current table definitions. If sending the schema stops partway, run the command again.
|
|
195
|
+
|
|
162
196
|
#### tailordb migration script
|
|
163
197
|
|
|
164
198
|
Add a migration script (migrate.ts) template to an existing migration directory, or record with --no-script that a migration intentionally has none.
|
package/docs/cli-reference.md
CHANGED
|
@@ -72,6 +72,16 @@ runnable command), or `error.context`. `UNEXPECTED_ERROR` marks failures without
|
|
|
72
72
|
including SDK-internal errors. Diagnostic lines may precede the error envelope, and stdout is not
|
|
73
73
|
guaranteed to contain an error object.
|
|
74
74
|
|
|
75
|
+
The exit code separates a mistyped command from a failed one without parsing output: `0` on success,
|
|
76
|
+
`2` when the CLI rejects the command line itself, such as an unknown option or subcommand, an
|
|
77
|
+
unexpected argument, or an option value that fails validation (including one set through its
|
|
78
|
+
environment variable), and `1` for other failures, including checks a command makes after reading
|
|
79
|
+
its arguments. A command that only warns about an unknown option or an extra argument keeps
|
|
80
|
+
running. Cancelling an interactive prompt exits with `130`, and a CLI plugin exits with its own
|
|
81
|
+
code. A command group run without a subcommand, such as `tailor` or `tailor tailordb`, can exit
|
|
82
|
+
with `0` after printing its help or a warning even when an option is unknown or its value is
|
|
83
|
+
invalid, so name the subcommand to rely on `2`.
|
|
84
|
+
|
|
75
85
|
Authentication failures distinguish missing credentials (`AUTH_TOKEN_NOT_FOUND`), a missing saved
|
|
76
86
|
user (`AUTH_USER_NOT_FOUND`), an expired token (`AUTH_TOKEN_EXPIRED`), and a failed token refresh
|
|
77
87
|
(`AUTH_TOKEN_REFRESH_FAILED`). Login recovery preserves the selected profile. For saved identities,
|
|
@@ -317,6 +327,7 @@ Commands for managing TailorDB tables, data, and schema migrations.
|
|
|
317
327
|
| [tailordb migration](./cli/tailordb.md#tailordb-migration) | Manage TailorDB schema migrations. |
|
|
318
328
|
| [tailordb migration generate](./cli/tailordb.md#tailordb-migration-generate) | Generate migration files by detecting schema differences between current local tables and the previous migration snapshot. |
|
|
319
329
|
| [tailordb migration rebaseline](./cli/tailordb.md#tailordb-migration-rebaseline) | Collapse the full migration history into a new 0000 baseline. |
|
|
330
|
+
| [tailordb migration resolve](./cli/tailordb.md#tailordb-migration-resolve) | Settle a migration that a failed deploy left partially applied. |
|
|
320
331
|
| [tailordb migration script](./cli/tailordb.md#tailordb-migration-script) | Add a migration script (migrate.ts) template to an existing migration directory, or record with --no-script that a migration intentionally has none. |
|
|
321
332
|
| [tailordb migration set](./cli/tailordb.md#tailordb-migration-set) | Set migration checkpoint to a specific number. |
|
|
322
333
|
| [tailordb migration status](./cli/tailordb.md#tailordb-migration-status) | Show the current migration status for TailorDB namespaces, including applied and pending migrations. |
|
package/docs/configuration.md
CHANGED
|
@@ -60,7 +60,20 @@ export default defineConfig({
|
|
|
60
60
|
|
|
61
61
|
Entries are only added or overwritten. An entry removed from the config keeps its last deployed value on the platform, and labels the config does not name are left untouched. Because those retained labels count towards the platform's limit of 20 labels per resource, `deploy` reports the overflow and stops before changing the application when the labels it would leave behind exceed that limit. The labels are written when the application itself is deployed, so a config with no TailorDB, Resolver, IdP, or Auth service has no application to carry them.
|
|
62
62
|
|
|
63
|
-
**Build Options**: `buildOptions` groups the settings that control how resolvers, executors, workflow jobs, and other functions are bundled: `logLevel` (below), `inlineSourcemap` (whether bundled functions embed an inline sourcemap for readable error stack traces; default `true`),
|
|
63
|
+
**Build Options**: `buildOptions` groups the settings that control how resolvers, executors, workflow jobs, and other functions are bundled: `logLevel` (below), `inlineSourcemap` (whether bundled functions embed an inline sourcemap for readable error stack traces; default `true`), `inlineSourcemapDependencySources` (below), `allowedRuntimeGlobals` (see [Node-only globals](#node-only-globals)), and `treeshake` (see [Removing unused code](#removing-unused-code)). The top-level `logLevel` and `inlineSourcemap` fields still work but are deprecated; `tailor upgrade` moves them into `buildOptions`. Setting the same option both at the top level and in `buildOptions` is rejected.
|
|
64
|
+
|
|
65
|
+
**Sourcemap Sources** (`buildOptions.inlineSourcemapDependencySources`): Whether the inline sourcemap embeds the source text of installed packages, that is, everything under `node_modules`. The default is `false`: only your own source files are embedded, which makes bundled functions much smaller. Error positions in stack traces resolve to the original files and lines either way. With the default, `tailor function run` and `tailor function logs` show the code snippet for a frame inside an installed package from the package's file on your machine, so the snippet is missing when that file is not installed locally (for `tailor function logs`, run it from the project directory you deployed from). Set it to `true` to embed the source text of installed packages as well, or to a list of package names such as `["kysely", "@scope/lib"]` to embed only those packages. A listed package's own dependencies are not embedded unless they are listed too. It has no effect when `inlineSourcemap` is `false`.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
export default defineConfig({
|
|
69
|
+
name: "my-app",
|
|
70
|
+
buildOptions: {
|
|
71
|
+
inlineSourcemapDependencySources: true,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This is a bundle-time setting. It affects newly bundled deployments; already deployed functions keep the sourcemap they were deployed with until they are redeployed.
|
|
64
77
|
|
|
65
78
|
**Log Level** (`buildOptions.logLevel`): Controls which `console.*` and `logger.*` (from `@tailor-platform/sdk/runtime`) calls are kept when deployment functions are bundled. Supported values are `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`, and `"SILENT"`. The default is `"DEBUG"` and keeps all calls. `console.log` is treated as a DEBUG-level call (matching the platform's OpenTelemetry severity mapping), so it is dropped at `"INFO"` and above, alongside `console.debug` and `logger.debug`. `logger.setAttributes` has no severity and is never dropped, regardless of `logLevel`. For production deployments, use `"WARN"` to keep warn/error calls while dropping debug, log, and info calls:
|
|
66
79
|
|
|
@@ -168,6 +181,44 @@ export default defineConfig({
|
|
|
168
181
|
|
|
169
182
|
`buildOptions.allowedRuntimeGlobals` has no effect on your own code. Packages from your own workspace (for example, a pnpm or npm workspace) are bundled from their source directory rather than from `node_modules`, so they count as your own code.
|
|
170
183
|
|
|
184
|
+
#### Removing unused code
|
|
185
|
+
|
|
186
|
+
Bundling leaves out code a function never reaches. By default, reading a property such as `shared.db.company` counts as something that may have an effect, so a call like `createService({ db: { company: shared.db.company } })` stays in the bundle even when nothing uses its result, together with every module it imports. When a bundle is larger than expected because of modules the function does not use, set `buildOptions.treeshake.propertyReadSideEffects` to `false`:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
export default defineConfig({
|
|
190
|
+
name: "my-app",
|
|
191
|
+
buildOptions: {
|
|
192
|
+
treeshake: { propertyReadSideEffects: false },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
With `false`, a property read whose result is unused is removed, and so is a call that only exists to receive such a read. This also removes a read that exists only for its effect: a getter that initializes something when it is read, or a statement such as `settings.db.url;` that throws when `settings.db` is missing. Keep the default if your code or an installed package relies on a read like that.
|
|
198
|
+
|
|
199
|
+
`treeshake` has four switches that control how unused code is removed from bundles. The values below are the SDK's defaults, which are tuned for the Tailor Platform runtime and rarely need changing. Most switches ask whether something has side effects, so `true` keeps it, while `annotations` asks whether to honor annotations, so `true` removes more:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
export default defineConfig({
|
|
203
|
+
name: "my-app",
|
|
204
|
+
buildOptions: {
|
|
205
|
+
treeshake: {
|
|
206
|
+
// true: keep an imported module even when none of its exports is used, so code
|
|
207
|
+
// that runs on import (such as registering something on `globalThis`) stays.
|
|
208
|
+
moduleSideEffects: false,
|
|
209
|
+
// true: honor `@__PURE__` and `@__NO_SIDE_EFFECTS__` comment annotations.
|
|
210
|
+
annotations: true,
|
|
211
|
+
// true: keep a read of a global the bundle does not define even when its result is unused.
|
|
212
|
+
unknownGlobalSideEffects: false,
|
|
213
|
+
// true: keep a property read (`shared.db.company`) even when its result is unused.
|
|
214
|
+
propertyReadSideEffects: true,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The settings apply to resolvers, executors, workflow jobs, auth hooks, HTTP adapters, and `tailor function run`; other bundles are not affected. They are bundle-time settings, so already deployed functions must be redeployed to pick up a change.
|
|
221
|
+
|
|
171
222
|
### External Resources
|
|
172
223
|
|
|
173
224
|
You can reference resources managed by Terraform or other SDK projects to include them in your application's subgraph. External resources are not deployed by this project but can be used for shared access across multiple applications.
|
|
@@ -454,8 +505,8 @@ Configure plugins using `definePlugins()`, exported as `export const plugins`.
|
|
|
454
505
|
|
|
455
506
|
```typescript
|
|
456
507
|
import { definePlugins } from "@tailor-platform/sdk";
|
|
457
|
-
import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";
|
|
458
508
|
import { enumConstantsPlugin } from "@tailor-platform/sdk/plugin/enum-constants";
|
|
509
|
+
import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";
|
|
459
510
|
|
|
460
511
|
export const plugins = definePlugins(
|
|
461
512
|
kyselyTypePlugin({ distPath: "./generated/tailordb.ts" }),
|