@palettelab/cli 0.3.45 → 0.3.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/backend-sdk/palette_sdk/storage.py +70 -29
- package/docs/python-backend-sdk.md +2 -0
- package/lib/commands/build.js +59 -18
- package/lib/commands/dev.js +23 -0
- package/lib/commands/package.js +19 -0
- package/lib/commands/publish.js +10 -3
- package/lib/commands/test.js +19 -2
- package/package.json +1 -1
- package/template-fallback/package.json +1 -1
- package/template-fallback/templates/dashboard/package.json +1 -1
- package/template-fallback/templates/database/package.json +1 -1
- package/template-fallback/templates/external-service/package.json +1 -1
- package/template-fallback/templates/frontend-only/package.json +1 -1
- package/template-fallback/templates/next/package.json +1 -1
- package/template-fallback/templates/palette-app/package.json +1 -1
package/README.md
CHANGED
|
@@ -328,6 +328,8 @@ async def sync_invoices(ctx: PluginContext = Depends(get_plugin_context)):
|
|
|
328
328
|
return {"room": room, "folder": folder, "bytes": len(content or b"")}
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
+
Storage upload responses include both snake_case and camelCase aliases for object metadata (`object_path`/`objectPath`, `file_url`/`fileUrl`, `content_type`/`contentType`) so local simulator and hosted OS responses can be consumed with the same app code.
|
|
332
|
+
|
|
331
333
|
App storage is different from Data Room uploads. Use `ctx.storage` or `palette.storage` for app-owned files written directly to the OS-configured storage backend, currently GCS in hosted environments. Use `ctx.data_rooms` / `palette.dataRooms` only when the file should be managed as a Data Room document.
|
|
332
334
|
|
|
333
335
|
Python backend app-storage example:
|
|
@@ -83,6 +83,47 @@ def _content_type(filename: str | None, content_type: str | None) -> str:
|
|
|
83
83
|
return browser_ct
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def _storage_metadata(
|
|
87
|
+
*,
|
|
88
|
+
bucket: str,
|
|
89
|
+
object_path: str,
|
|
90
|
+
file_url: str,
|
|
91
|
+
content_type: str,
|
|
92
|
+
size: int | None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
return {
|
|
95
|
+
"bucket": bucket,
|
|
96
|
+
"object_path": object_path,
|
|
97
|
+
"objectPath": object_path,
|
|
98
|
+
"file_url": file_url,
|
|
99
|
+
"fileUrl": file_url,
|
|
100
|
+
"content_type": content_type,
|
|
101
|
+
"contentType": content_type,
|
|
102
|
+
"size": size,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resumable_metadata(
|
|
107
|
+
*,
|
|
108
|
+
bucket: str,
|
|
109
|
+
object_path: str,
|
|
110
|
+
file_url: str,
|
|
111
|
+
upload_url: str | None,
|
|
112
|
+
content_type: str,
|
|
113
|
+
size: int | None,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
payload = _storage_metadata(
|
|
116
|
+
bucket=bucket,
|
|
117
|
+
object_path=object_path,
|
|
118
|
+
file_url=file_url,
|
|
119
|
+
content_type=content_type,
|
|
120
|
+
size=size,
|
|
121
|
+
)
|
|
122
|
+
payload["upload_url"] = upload_url
|
|
123
|
+
payload["uploadUrl"] = upload_url
|
|
124
|
+
return payload
|
|
125
|
+
|
|
126
|
+
|
|
86
127
|
@dataclass
|
|
87
128
|
class LocalStorageService:
|
|
88
129
|
"""Filesystem-backed ctx.storage implementation used by local SDK dev."""
|
|
@@ -122,13 +163,13 @@ class LocalStorageService:
|
|
|
122
163
|
target = self._target(object_path)
|
|
123
164
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
124
165
|
target.write_bytes(content)
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
return _storage_metadata(
|
|
167
|
+
bucket="local",
|
|
168
|
+
object_path=object_path,
|
|
169
|
+
file_url=target.as_uri(),
|
|
170
|
+
content_type=_content_type(filename, content_type),
|
|
171
|
+
size=len(content),
|
|
172
|
+
)
|
|
132
173
|
|
|
133
174
|
async def upload_file_stream(
|
|
134
175
|
self,
|
|
@@ -168,13 +209,13 @@ class LocalStorageService:
|
|
|
168
209
|
chunk_count=chunk_count,
|
|
169
210
|
state="complete",
|
|
170
211
|
)
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
212
|
+
return _storage_metadata(
|
|
213
|
+
bucket="local",
|
|
214
|
+
object_path=object_path,
|
|
215
|
+
file_url=target.as_uri(),
|
|
216
|
+
content_type=_content_type(filename, content_type),
|
|
217
|
+
size=0,
|
|
218
|
+
)
|
|
178
219
|
with target.open("wb") as out:
|
|
179
220
|
chunk_index = 0
|
|
180
221
|
while True:
|
|
@@ -194,13 +235,13 @@ class LocalStorageService:
|
|
|
194
235
|
)
|
|
195
236
|
if loaded != total:
|
|
196
237
|
raise RuntimeError(f"storage upload incomplete: uploaded {loaded} of {total} bytes")
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
238
|
+
return _storage_metadata(
|
|
239
|
+
bucket="local",
|
|
240
|
+
object_path=object_path,
|
|
241
|
+
file_url=target.as_uri(),
|
|
242
|
+
content_type=_content_type(filename, content_type),
|
|
243
|
+
size=loaded,
|
|
244
|
+
)
|
|
204
245
|
|
|
205
246
|
async def upload_file_path(
|
|
206
247
|
self,
|
|
@@ -234,11 +275,11 @@ class LocalStorageService:
|
|
|
234
275
|
key: str | None = None,
|
|
235
276
|
) -> dict[str, Any]:
|
|
236
277
|
object_path = self.object_path(filename, key=key)
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
278
|
+
return _resumable_metadata(
|
|
279
|
+
bucket="local",
|
|
280
|
+
object_path=object_path,
|
|
281
|
+
file_url=self._target(object_path).as_uri(),
|
|
282
|
+
upload_url=None,
|
|
283
|
+
content_type=_content_type(filename, content_type),
|
|
284
|
+
size=size,
|
|
285
|
+
)
|
|
@@ -1044,3 +1044,5 @@ async def create_note(
|
|
|
1044
1044
|
|
|
1045
1045
|
This route stores app-owned data in the app database and writes a related file
|
|
1046
1046
|
into Palette Data Rooms from the Python backend.
|
|
1047
|
+
|
|
1048
|
+
Backend storage responses include both snake_case and camelCase aliases for object metadata (`object_path`/`objectPath`, `file_url`/`fileUrl`, `content_type`/`contentType`). The concrete values differ by environment, but the key contract is stable across local dev and hosted OS.
|
package/lib/commands/build.js
CHANGED
|
@@ -31,6 +31,22 @@ function pluginTablePrefix(pluginId) {
|
|
|
31
31
|
return `${pluginSafeId(pluginId)}__`
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function makeIssue(message, fix = null, meta = {}) {
|
|
35
|
+
return { message, fix, ...meta }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatIssue(issue) {
|
|
39
|
+
if (typeof issue === "string") return issue
|
|
40
|
+
const lines = [issue.message || String(issue)]
|
|
41
|
+
if (issue.details) lines.push(`Why: ${issue.details}`)
|
|
42
|
+
if (issue.fix) lines.push(`Fix: ${issue.fix}`)
|
|
43
|
+
if (issue.example) {
|
|
44
|
+
lines.push("Example:")
|
|
45
|
+
for (const line of String(issue.example).split("\n")) lines.push(` ${line}`)
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n ")
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
function crossPluginSchemaRefs(src, allowedSchema) {
|
|
35
51
|
if (!allowedSchema) return []
|
|
36
52
|
const refs = new Set()
|
|
@@ -43,28 +59,27 @@ function crossPluginSchemaRefs(src, allowedSchema) {
|
|
|
43
59
|
return [...refs].sort()
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
function lintMigrationFile(absPath, pluginId) {
|
|
62
|
+
function lintMigrationFile(absPath, pluginId, sharedTables = []) {
|
|
47
63
|
const issues = []
|
|
48
64
|
const src = fs.readFileSync(absPath, "utf8")
|
|
49
65
|
const requiredPrefix = pluginId ? pluginTablePrefix(pluginId) : null
|
|
50
66
|
const allowedSchema = pluginId ? pluginSchema(pluginId) : null
|
|
67
|
+
const sharedTableNames = new Set(sharedTables || [])
|
|
51
68
|
|
|
52
69
|
for (const { re, reason } of BANNED_PATTERNS) {
|
|
53
70
|
if (re.test(src)) {
|
|
54
|
-
issues.push(`${path.basename(absPath)}: ${reason}`)
|
|
71
|
+
issues.push(makeIssue(`${path.basename(absPath)}: ${reason}`))
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
for (const schema of crossPluginSchemaRefs(src, allowedSchema)) {
|
|
59
|
-
issues.push(`${path.basename(absPath)}: plugin migrations must not reference another app schema (${schema})`)
|
|
76
|
+
issues.push(makeIssue(`${path.basename(absPath)}: plugin migrations must not reference another app schema (${schema})`))
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
// Every op.create_table("foo", ...) in the file must have a matching
|
|
63
|
-
// ensure_org_rls(op, "foo") somewhere in the same file
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// magic comment `# palette:rls-ok` on the same logical migration.
|
|
67
|
-
const skipRlsCheck = /#\s*palette:rls-ok\b/.test(src)
|
|
80
|
+
// ensure_org_rls(op, "foo") somewhere in the same file unless the table is
|
|
81
|
+
// declared in manifest.shared_tables. Caveat: this is a cheap syntactic
|
|
82
|
+
// check, not a full AST walk.
|
|
68
83
|
|
|
69
84
|
const tableNames = new Set()
|
|
70
85
|
const createTableRe = /op\.create_table\(\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g
|
|
@@ -75,23 +90,48 @@ function lintMigrationFile(absPath, pluginId) {
|
|
|
75
90
|
|
|
76
91
|
for (const name of tableNames) {
|
|
77
92
|
if (requiredPrefix && !name.startsWith(requiredPrefix)) {
|
|
78
|
-
issues.push(
|
|
93
|
+
issues.push(makeIssue(
|
|
79
94
|
`${path.basename(absPath)}: create_table("${name}") must use the app table prefix "${requiredPrefix}"`,
|
|
80
|
-
|
|
95
|
+
`Rename the table to "${requiredPrefix}${name}" or another name that starts with "${requiredPrefix}", then update model and query references.`,
|
|
96
|
+
{
|
|
97
|
+
code: "database_table_prefix",
|
|
98
|
+
file: path.basename(absPath),
|
|
99
|
+
table: name,
|
|
100
|
+
required_prefix: requiredPrefix,
|
|
101
|
+
},
|
|
102
|
+
))
|
|
81
103
|
}
|
|
82
104
|
const rlsRe = new RegExp(`ensure_org_rls\\(\\s*op\\s*,\\s*['"]${name}['"]`)
|
|
83
|
-
if (!
|
|
84
|
-
|
|
105
|
+
if (!sharedTableNames.has(name) && !rlsRe.test(src)) {
|
|
106
|
+
const schemaHint = pluginId ? `${pluginSchema(pluginId)}.${name}` : name
|
|
107
|
+
issues.push(makeIssue(
|
|
85
108
|
`${path.basename(absPath)}: create_table("${name}") is missing ensure_org_rls(op, "${name}"). ` +
|
|
86
|
-
`
|
|
87
|
-
|
|
109
|
+
`Preview/install will fail RLS compliance for ${schemaHint}: missing ENABLE + FORCE row level security.`,
|
|
110
|
+
`Import ensure_org_rls from palette_sdk.db and call ensure_org_rls(op, "${name}") immediately after op.create_table("${name}", ...). ` +
|
|
111
|
+
`Keep an organization_id column on tenant data tables. If this table is intentionally shared across every organization, add "${name}" to manifest.shared_tables instead and document why it is global.`,
|
|
112
|
+
{
|
|
113
|
+
code: "database_missing_org_rls",
|
|
114
|
+
file: path.basename(absPath),
|
|
115
|
+
table: name,
|
|
116
|
+
schema: pluginId ? pluginSchema(pluginId) : null,
|
|
117
|
+
details:
|
|
118
|
+
"Palette verifies every plugin table after migrations run. Tables that hold tenant data must have organization_id, at least one RLS policy, and ENABLE + FORCE row level security.",
|
|
119
|
+
example: [
|
|
120
|
+
"from palette_sdk.db import ensure_org_rls",
|
|
121
|
+
"",
|
|
122
|
+
"def upgrade():",
|
|
123
|
+
` op.create_table("${name}", ...)`,
|
|
124
|
+
` ensure_org_rls(op, "${name}")`,
|
|
125
|
+
].join("\n"),
|
|
126
|
+
},
|
|
127
|
+
))
|
|
88
128
|
}
|
|
89
129
|
}
|
|
90
130
|
|
|
91
131
|
return issues
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
function lintMigrationsDir(migrationsDir, pluginId) {
|
|
134
|
+
function lintMigrationsDir(migrationsDir, pluginId, sharedTables = []) {
|
|
95
135
|
const errors = []
|
|
96
136
|
const versionsDir = path.join(migrationsDir, "versions")
|
|
97
137
|
if (!fs.existsSync(versionsDir)) {
|
|
@@ -101,7 +141,7 @@ function lintMigrationsDir(migrationsDir, pluginId) {
|
|
|
101
141
|
for (const entry of fs.readdirSync(versionsDir)) {
|
|
102
142
|
if (!entry.endsWith(".py")) continue
|
|
103
143
|
const abs = path.join(versionsDir, entry)
|
|
104
|
-
errors.push(...lintMigrationFile(abs, pluginId))
|
|
144
|
+
errors.push(...lintMigrationFile(abs, pluginId, sharedTables))
|
|
105
145
|
}
|
|
106
146
|
return errors
|
|
107
147
|
}
|
|
@@ -127,13 +167,13 @@ async function run(args, { cwd }) {
|
|
|
127
167
|
} else if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
128
168
|
errors.push(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
129
169
|
} else {
|
|
130
|
-
errors.push(...lintMigrationsDir(migrationsAbs, manifest.id))
|
|
170
|
+
errors.push(...lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || []))
|
|
131
171
|
}
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
if (errors.length) {
|
|
135
175
|
console.error("[pltt] validation failed:")
|
|
136
|
-
for (const e of errors) console.error(` - ${e}`)
|
|
176
|
+
for (const e of errors) console.error(` - ${formatIssue(e)}`)
|
|
137
177
|
process.exit(1)
|
|
138
178
|
}
|
|
139
179
|
console.log(`[pltt] ok — ${manifest.id} v${manifest.version}`)
|
|
@@ -142,4 +182,5 @@ async function run(args, { cwd }) {
|
|
|
142
182
|
module.exports = run
|
|
143
183
|
module.exports.lintMigrationsDir = lintMigrationsDir
|
|
144
184
|
module.exports.lintMigrationFile = lintMigrationFile
|
|
185
|
+
module.exports.formatIssue = formatIssue
|
|
145
186
|
module.exports.pluginTablePrefix = pluginTablePrefix
|
package/lib/commands/dev.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
|
+
const fs = require("fs")
|
|
3
4
|
const path = require("path")
|
|
4
5
|
const { spawn, spawnSync } = require("child_process")
|
|
5
6
|
const { loadManifest } = require("../manifest")
|
|
@@ -8,6 +9,7 @@ const { parseFlags, resolveEnvironment } = require("../environments")
|
|
|
8
9
|
const { resolveDevPorts } = require("../ports")
|
|
9
10
|
const { startSimulator } = require("../dev-simulator")
|
|
10
11
|
const { loadLocalEnv } = require("../secrets")
|
|
12
|
+
const buildCommand = require("./build")
|
|
11
13
|
const publish = require("./publish")
|
|
12
14
|
const logs = require("./logs")
|
|
13
15
|
|
|
@@ -76,6 +78,26 @@ function imagePullHelp(image, output) {
|
|
|
76
78
|
)
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
function lintMigrationsForDev(cwd, manifest) {
|
|
82
|
+
if (!manifest.database) return
|
|
83
|
+
const migrationsRel = manifest.database.migrations || "./backend/migrations"
|
|
84
|
+
const migrationsAbs = path.resolve(cwd, migrationsRel)
|
|
85
|
+
const errors = []
|
|
86
|
+
if (!fs.existsSync(migrationsAbs)) {
|
|
87
|
+
errors.push(`database.migrations directory not found: ${migrationsRel}`)
|
|
88
|
+
} else if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
89
|
+
errors.push(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
90
|
+
} else {
|
|
91
|
+
errors.push(...buildCommand.lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || []))
|
|
92
|
+
}
|
|
93
|
+
if (!errors.length) return
|
|
94
|
+
|
|
95
|
+
console.error("[pltt] dev blocked by database migration validation:")
|
|
96
|
+
for (const err of errors) console.error(` - ${buildCommand.formatIssue(err)}`)
|
|
97
|
+
console.error("[pltt] Fix these issues or run `pltt test --json` for machine-readable details.")
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
async function run(args, { cwd }) {
|
|
80
102
|
const { flags, rest } = parseFlags(args)
|
|
81
103
|
const cloud = rest.includes("--cloud") || rest.includes("--sandbox")
|
|
@@ -124,6 +146,7 @@ async function run(args, { cwd }) {
|
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
const manifest = loadManifest(cwd)
|
|
149
|
+
lintMigrationsForDev(cwd, manifest)
|
|
127
150
|
const pluginId = manifest.id
|
|
128
151
|
const ports = await resolveDevPorts({ host: platform ? "0.0.0.0" : "127.0.0.1" })
|
|
129
152
|
|
package/lib/commands/package.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require("path")
|
|
|
5
5
|
const crypto = require("crypto")
|
|
6
6
|
const { loadManifest, validateManifest } = require("../manifest")
|
|
7
7
|
const { bundleFrontend, bundleBackend } = require("../bundler")
|
|
8
|
+
const buildCommand = require("./build")
|
|
8
9
|
|
|
9
10
|
function sha256(buf) {
|
|
10
11
|
return crypto.createHash("sha256").update(buf).digest("hex")
|
|
@@ -20,6 +21,24 @@ async function run(argv, { cwd }) {
|
|
|
20
21
|
process.exit(1)
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
if (manifest.database) {
|
|
25
|
+
const migrationsRel = manifest.database.migrations || "./backend/migrations"
|
|
26
|
+
const migrationsAbs = path.resolve(cwd, migrationsRel)
|
|
27
|
+
const migrationErrors = []
|
|
28
|
+
if (!fs.existsSync(migrationsAbs)) {
|
|
29
|
+
migrationErrors.push(`database.migrations directory not found: ${migrationsRel}`)
|
|
30
|
+
} else if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
31
|
+
migrationErrors.push(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
32
|
+
} else {
|
|
33
|
+
migrationErrors.push(...buildCommand.lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || []))
|
|
34
|
+
}
|
|
35
|
+
if (migrationErrors.length) {
|
|
36
|
+
console.error("[pltt] package blocked by database migration validation:")
|
|
37
|
+
for (const e of migrationErrors) console.error(` - ${buildCommand.formatIssue(e)}`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
const distDir = path.join(cwd, "dist")
|
|
24
43
|
fs.mkdirSync(distDir, { recursive: true })
|
|
25
44
|
|
package/lib/commands/publish.js
CHANGED
|
@@ -80,14 +80,21 @@ function explainPreflightFailure(result) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const migration =
|
|
83
|
+
const migration =
|
|
84
|
+
result?.code === "database_missing_org_rls" && result?.table
|
|
85
|
+
? { file: result.file || "migration", table: result.table }
|
|
86
|
+
: extractMigrationHint(message)
|
|
84
87
|
if (migration) {
|
|
85
88
|
return {
|
|
86
89
|
title: "Database migration is missing organization RLS",
|
|
87
|
-
details: [
|
|
90
|
+
details: [
|
|
91
|
+
`File: ${migration.file}`,
|
|
92
|
+
`Table: ${migration.table}`,
|
|
93
|
+
"Preview/install will reject this table unless RLS is enabled and forced.",
|
|
94
|
+
],
|
|
88
95
|
fix:
|
|
89
96
|
fix ||
|
|
90
|
-
`Call ensure_org_rls(op, "${migration.table}") after create_table, or add
|
|
97
|
+
`Call ensure_org_rls(op, "${migration.table}") after create_table, or add "${migration.table}" to manifest.shared_tables only if the table is intentionally global.`,
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
package/lib/commands/test.js
CHANGED
|
@@ -12,6 +12,14 @@ const DEFAULT_FRONTEND_BUNDLE_LIMIT = 15 * 1024 * 1024
|
|
|
12
12
|
const DEFAULT_BACKEND_BUNDLE_LIMIT = 15 * 1024 * 1024
|
|
13
13
|
|
|
14
14
|
function reporter(json, results) {
|
|
15
|
+
function printExtra(meta) {
|
|
16
|
+
if (meta.details) console.log(`WHY ${meta.details}`)
|
|
17
|
+
if (meta.example) {
|
|
18
|
+
console.log("EXAMPLE")
|
|
19
|
+
for (const line of String(meta.example).split("\n")) console.log(` ${line}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
return {
|
|
16
24
|
ok(message, meta = {}) {
|
|
17
25
|
results.push({ status: "ok", message, ...meta })
|
|
@@ -22,6 +30,7 @@ function reporter(json, results) {
|
|
|
22
30
|
if (!json) {
|
|
23
31
|
console.log(`WARN ${message}`)
|
|
24
32
|
if (fix) console.log(`FIX ${fix}`)
|
|
33
|
+
printExtra(meta)
|
|
25
34
|
}
|
|
26
35
|
return 0
|
|
27
36
|
},
|
|
@@ -30,6 +39,7 @@ function reporter(json, results) {
|
|
|
30
39
|
if (!json) {
|
|
31
40
|
console.log(`FAIL ${message}`)
|
|
32
41
|
if (fix) console.log(`FIX ${fix}`)
|
|
42
|
+
printExtra(meta)
|
|
33
43
|
}
|
|
34
44
|
return 1
|
|
35
45
|
},
|
|
@@ -253,8 +263,15 @@ function lintMigrations(cwd, manifest, out) {
|
|
|
253
263
|
if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
254
264
|
return out.fail(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
255
265
|
}
|
|
256
|
-
const errors = buildCommand.lintMigrationsDir(migrationsAbs, manifest.id)
|
|
257
|
-
for (const err of errors)
|
|
266
|
+
const errors = buildCommand.lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || [])
|
|
267
|
+
for (const err of errors) {
|
|
268
|
+
if (typeof err === "string") {
|
|
269
|
+
out.fail(err)
|
|
270
|
+
} else {
|
|
271
|
+
const { message, fix, ...meta } = err
|
|
272
|
+
out.fail(message, fix, meta)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
258
275
|
if (errors.length === 0) out.ok("migration lint passed")
|
|
259
276
|
return errors.length
|
|
260
277
|
}
|
package/package.json
CHANGED