296 lines
8.7 KiB
Bash
Executable File
296 lines
8.7 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
./script/export-collection-schema.sh \
|
|
--env <environment> \
|
|
--output-dir <directory> \
|
|
[--category <NocoBase category name>] \
|
|
[--no-overwrite]
|
|
|
|
Options:
|
|
--env Required NocoBase CLI environment name.
|
|
--output-dir Required export destination.
|
|
--category Exact NocoBase Collection category name. Default: all.
|
|
--no-overwrite Refuse to replace an existing output directory.
|
|
Default: atomically replace the existing directory.
|
|
-h, --help Show this help.
|
|
|
|
Portable export of the main data source is always enabled. Output files are JSON.
|
|
|
|
Example:
|
|
./script/export-collection-schema.sh \
|
|
--env master-data-local \
|
|
--category 参考数据 \
|
|
--output-dir ./exports/reference-schema
|
|
EOF
|
|
}
|
|
|
|
ENV_NAME=
|
|
OUTPUT_DIR=
|
|
CATEGORY=
|
|
NO_OVERWRITE=0
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--env)
|
|
ENV_NAME=${2:?--env requires a value}
|
|
shift 2
|
|
;;
|
|
--output-dir)
|
|
OUTPUT_DIR=${2:?--output-dir requires a value}
|
|
shift 2
|
|
;;
|
|
--category)
|
|
CATEGORY=${2:?--category requires a value}
|
|
shift 2
|
|
;;
|
|
--no-overwrite)
|
|
NO_OVERWRITE=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$ENV_NAME" ] || { echo "--env is required" >&2; exit 2; }
|
|
[ -n "$OUTPUT_DIR" ] || { echo "--output-dir is required" >&2; exit 2; }
|
|
command -v nb >/dev/null 2>&1 || { echo "nb CLI is required" >&2; exit 2; }
|
|
command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2; exit 2; }
|
|
|
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
PROJECT_ROOT=$(dirname -- "$SCRIPT_DIR")
|
|
OUTPUT_PARENT=$(dirname -- "$OUTPUT_DIR")
|
|
OUTPUT_NAME=$(basename -- "$OUTPUT_DIR")
|
|
[ "$OUTPUT_NAME" != . ] && [ "$OUTPUT_NAME" != .. ] || {
|
|
echo "Refusing unsafe --output-dir: $OUTPUT_DIR" >&2
|
|
exit 2
|
|
}
|
|
mkdir -p "$OUTPUT_PARENT"
|
|
OUTPUT_PARENT=$(CDPATH= cd -- "$OUTPUT_PARENT" && pwd)
|
|
OUTPUT_DIR="$OUTPUT_PARENT/$OUTPUT_NAME"
|
|
|
|
case "$OUTPUT_DIR" in
|
|
/|"$HOME"|"$PROJECT_ROOT"|"$SCRIPT_DIR")
|
|
echo "Refusing unsafe --output-dir: $OUTPUT_DIR" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
if [ "$NO_OVERWRITE" -eq 1 ] && [ -e "$OUTPUT_DIR" ]; then
|
|
echo "Output directory already exists: $OUTPUT_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
run_nb() {
|
|
RAW_OUTPUT=$("$@")
|
|
JSON_OUTPUT=$(printf '%s\n' "$RAW_OUTPUT" | awk '
|
|
!started && /^[[:space:]]*[\{\[]/ { started = 1 }
|
|
started { print }
|
|
')
|
|
printf '%s\n' "$JSON_OUTPUT" | jq -e . >/dev/null
|
|
printf '%s\n' "$JSON_OUTPUT"
|
|
}
|
|
|
|
validate_segment() {
|
|
case "$1" in
|
|
''|.|..|*/*|*\\*)
|
|
echo "Unsafe path segment: $1" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
list_collections() {
|
|
PAGE=1
|
|
ALL='[]'
|
|
while :; do
|
|
RESPONSE=$(run_nb nb api data-modeling collections list \
|
|
--page "$PAGE" --page-size 500 -e "$ENV_NAME" -y -j)
|
|
ROWS=$(printf '%s' "$RESPONSE" | jq -c '.data // []')
|
|
ALL=$(jq -cn --argjson current "$ALL" --argjson rows "$ROWS" '$current + $rows')
|
|
TOTAL_PAGES=$(printf '%s' "$RESPONSE" | jq -r '.meta.totalPage // 1')
|
|
[ "$PAGE" -ge "$TOTAL_PAGES" ] && break
|
|
PAGE=$((PAGE + 1))
|
|
done
|
|
printf '%s\n' "$ALL"
|
|
}
|
|
|
|
load_categories() {
|
|
run_nb nb api resource list \
|
|
--resource collectionCategories \
|
|
--no-paginate \
|
|
--sort sort \
|
|
--fields name \
|
|
--appends collections \
|
|
-e "$ENV_NAME" -y -j
|
|
}
|
|
|
|
read_collection() {
|
|
COLLECTION_NAME=$1
|
|
run_nb nb api data-modeling collections get \
|
|
--filter-by-tk "$COLLECTION_NAME" \
|
|
--appends fields \
|
|
-e "$ENV_NAME" -y -j
|
|
}
|
|
|
|
portable_schema() {
|
|
jq '
|
|
.data as $collection
|
|
| ($collection.fields // []) as $fields
|
|
| [$fields[]
|
|
| select(.interface == "m2o" or .interface == "o2m" or .interface == "m2m" or .interface == "o2o")
|
|
| .foreignKey
|
|
| select(. != null)] as $foreignKeys
|
|
| ($collection.template // "general") as $template
|
|
| ($collection | del(.key, .fields, .verify, .category))
|
|
+ {
|
|
fields: [
|
|
$fields[]
|
|
| . as $field
|
|
| select(["id", "createdAt", "createdBy", "updatedAt", "updatedBy", "createdById", "updatedById"] | index($field.name) | not)
|
|
| select(
|
|
if $template == "file" then
|
|
["title", "filename", "extname", "size", "mimetype", "path", "url", "preview", "storage", "storageId", "meta"]
|
|
| index($field.name) | not
|
|
elif $template == "tree" then
|
|
["parentId", "parent", "children"] | index($field.name) | not
|
|
else true end
|
|
)
|
|
| select(
|
|
($foreignKeys | index($field.name) | not)
|
|
or ($field.interface == "m2o" or $field.interface == "o2m" or $field.interface == "m2m" or $field.interface == "o2o")
|
|
)
|
|
| del(.key, .collectionName, .parentKey, .reverseKey, .possibleTypes)
|
|
| walk(
|
|
if type == "object" and has("rules") and (.rules | type) == "array" then
|
|
.rules |= map(del(.key))
|
|
else . end
|
|
)
|
|
] | sort_by(.name)
|
|
}
|
|
'
|
|
}
|
|
|
|
COLLECTIONS=$(list_collections)
|
|
CATEGORIES=$(load_categories)
|
|
|
|
KNOWN_CATEGORIES=$(printf '%s' "$CATEGORIES" | jq -r '[.data[]?.name, "未分类"] | unique | join(", ")')
|
|
if [ -n "$CATEGORY" ] && ! printf '%s' "$CATEGORIES" | jq -e --arg category "$CATEGORY" '
|
|
any(.data[]?; .name == $category) or $category == "未分类"
|
|
' >/dev/null; then
|
|
echo "Unknown category: $CATEGORY; available: $KNOWN_CATEGORIES" >&2
|
|
exit 1
|
|
fi
|
|
|
|
SELECTION=$(printf '%s' "$COLLECTIONS" | jq -c \
|
|
--argjson categoryData "$CATEGORIES" \
|
|
--arg selected "$CATEGORY" '
|
|
[
|
|
.[]
|
|
| . as $collection
|
|
| [$categoryData.data[]?
|
|
| select(any(.collections[]?; .name == $collection.name))
|
|
| .name] as $assigned
|
|
| (if ($assigned | length) == 0 then ["未分类"] else $assigned end) as $categories
|
|
| {
|
|
collection: $collection,
|
|
categories: ($categories | map(select($selected == "" or . == $selected)))
|
|
}
|
|
| select((.categories | length) > 0)
|
|
]
|
|
')
|
|
|
|
TEMP_DIR=$(mktemp -d "$OUTPUT_PARENT/.${OUTPUT_NAME}.tmp.XXXXXX")
|
|
MANIFEST_ROWS="$TEMP_DIR/.manifest-rows.jsonl"
|
|
: > "$MANIFEST_ROWS"
|
|
trap 'rm -rf "$TEMP_DIR"' EXIT HUP INT TERM
|
|
|
|
INDEX=0
|
|
COUNT=$(printf '%s' "$SELECTION" | jq 'length')
|
|
while [ "$INDEX" -lt "$COUNT" ]; do
|
|
ITEM=$(printf '%s' "$SELECTION" | jq -c ".[$INDEX]")
|
|
COLLECTION_NAME=$(printf '%s' "$ITEM" | jq -r '.collection.name')
|
|
validate_segment "$COLLECTION_NAME"
|
|
SCHEMA=$(read_collection "$COLLECTION_NAME" | portable_schema)
|
|
CATEGORY_COUNT=$(printf '%s' "$ITEM" | jq '.categories | length')
|
|
CATEGORY_INDEX=0
|
|
while [ "$CATEGORY_INDEX" -lt "$CATEGORY_COUNT" ]; do
|
|
CATEGORY_NAME=$(printf '%s' "$ITEM" | jq -r ".categories[$CATEGORY_INDEX]")
|
|
validate_segment "$CATEGORY_NAME"
|
|
RELATIVE_FILE="main/$CATEGORY_NAME/$COLLECTION_NAME.json"
|
|
TARGET_FILE="$TEMP_DIR/$RELATIVE_FILE"
|
|
mkdir -p "$(dirname -- "$TARGET_FILE")"
|
|
printf '%s\n' "$SCHEMA" > "$TARGET_FILE"
|
|
jq -cn \
|
|
--arg dataSource main \
|
|
--arg category "$CATEGORY_NAME" \
|
|
--arg collection "$COLLECTION_NAME" \
|
|
--arg file "$RELATIVE_FILE" \
|
|
'{dataSource:$dataSource, category:$category, collection:$collection, file:$file}' \
|
|
>> "$MANIFEST_ROWS"
|
|
CATEGORY_INDEX=$((CATEGORY_INDEX + 1))
|
|
done
|
|
INDEX=$((INDEX + 1))
|
|
done
|
|
|
|
FILES=$(jq -s 'sort_by(.file)' "$MANIFEST_ROWS")
|
|
DISCOVERED=$(printf '%s' "$COLLECTIONS" | jq 'length')
|
|
EXPORTED_FILES=$(printf '%s' "$FILES" | jq 'length')
|
|
GENERATED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
OVERWRITE_POLICY=replace-existing
|
|
[ "$NO_OVERWRITE" -eq 1 ] && OVERWRITE_POLICY=refuse-existing
|
|
|
|
jq -n \
|
|
--arg environment "$ENV_NAME" \
|
|
--arg dataSource main \
|
|
--arg category "${CATEGORY:-all}" \
|
|
--arg generatedAt "$GENERATED_AT" \
|
|
--arg overwritePolicy "$OVERWRITE_POLICY" \
|
|
--argjson discoveredCollections "$DISCOVERED" \
|
|
--argjson exportedCollections "$COUNT" \
|
|
--argjson exportedFiles "$EXPORTED_FILES" \
|
|
--argjson files "$FILES" \
|
|
'{
|
|
environment:$environment,
|
|
dataSource:$dataSource,
|
|
category:$category,
|
|
profile:"portable",
|
|
generatedAt:$generatedAt,
|
|
overwritePolicy:$overwritePolicy,
|
|
discoveredCollections:$discoveredCollections,
|
|
exportedCollections:$exportedCollections,
|
|
exportedFiles:$exportedFiles,
|
|
files:$files
|
|
}' > "$TEMP_DIR/manifest.json"
|
|
|
|
rm -f "$MANIFEST_ROWS"
|
|
|
|
if [ -e "$OUTPUT_DIR" ]; then
|
|
BACKUP_DIR="$OUTPUT_DIR.previous.$$"
|
|
mv "$OUTPUT_DIR" "$BACKUP_DIR"
|
|
if mv "$TEMP_DIR" "$OUTPUT_DIR"; then
|
|
rm -rf "$BACKUP_DIR"
|
|
else
|
|
mv "$BACKUP_DIR" "$OUTPUT_DIR"
|
|
exit 1
|
|
fi
|
|
else
|
|
mv "$TEMP_DIR" "$OUTPUT_DIR"
|
|
fi
|
|
trap - EXIT HUP INT TERM
|
|
|
|
jq --arg outputDir "$OUTPUT_DIR" '
|
|
del(.files) + {outputDir:$outputDir}
|
|
' "$OUTPUT_DIR/manifest.json"
|