@nextsparkjs/ai-workflow 0.1.0-beta.100
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/LICENSE +21 -0
- package/README.md +115 -0
- package/claude/_docs/workflows-optimizations.md +359 -0
- package/claude/agents/api-tester.md +634 -0
- package/claude/agents/architecture-supervisor.md +1351 -0
- package/claude/agents/backend-developer.md +997 -0
- package/claude/agents/backend-validator.md +417 -0
- package/claude/agents/bdd-docs-writer.md +737 -0
- package/claude/agents/block-developer.md +677 -0
- package/claude/agents/code-reviewer.md +1432 -0
- package/claude/agents/db-developer.md +721 -0
- package/claude/agents/db-validator.md +407 -0
- package/claude/agents/demo-video-generator.md +493 -0
- package/claude/agents/documentation-writer.md +1268 -0
- package/claude/agents/frontend-developer.md +1234 -0
- package/claude/agents/frontend-validator.md +777 -0
- package/claude/agents/functional-validator.md +630 -0
- package/claude/agents/mock-analyst.md +387 -0
- package/claude/agents/product-manager.md +963 -0
- package/claude/agents/qa-automation.md +1762 -0
- package/claude/agents/release-manager.md +634 -0
- package/claude/agents/selectors-translator.md +262 -0
- package/claude/agents/unit-test-writer.md +785 -0
- package/claude/agents/visual-comparator.md +329 -0
- package/claude/agents/workflow-maintainer.md +352 -0
- package/claude/commands/do/README.md +88 -0
- package/claude/commands/do/create-api.md +64 -0
- package/claude/commands/do/create-entity.md +66 -0
- package/claude/commands/do/create-migration.md +64 -0
- package/claude/commands/do/create-plugin.md +56 -0
- package/claude/commands/do/create-theme.md +70 -0
- package/claude/commands/do/mock-data.md +67 -0
- package/claude/commands/do/reset-db.md +71 -0
- package/claude/commands/do/setup-scheduled-action.md +75 -0
- package/claude/commands/do/sync-code-review.md +117 -0
- package/claude/commands/do/update-selectors.md +112 -0
- package/claude/commands/do/use-skills.md +90 -0
- package/claude/commands/do/validate-blocks.md +69 -0
- package/claude/commands/how-to/README.md +261 -0
- package/claude/commands/how-to/add-metadata.md +692 -0
- package/claude/commands/how-to/add-taxonomies.md +806 -0
- package/claude/commands/how-to/add-translations.md +571 -0
- package/claude/commands/how-to/create-api.md +577 -0
- package/claude/commands/how-to/create-block.md +575 -0
- package/claude/commands/how-to/create-child-entities.md +771 -0
- package/claude/commands/how-to/create-entity.md +597 -0
- package/claude/commands/how-to/create-migrations.md +605 -0
- package/claude/commands/how-to/create-plugin.md +654 -0
- package/claude/commands/how-to/customize-app.md +481 -0
- package/claude/commands/how-to/customize-dashboard.md +553 -0
- package/claude/commands/how-to/customize-theme.md +438 -0
- package/claude/commands/how-to/define-features-flows.md +632 -0
- package/claude/commands/how-to/deploy.md +507 -0
- package/claude/commands/how-to/handle-file-uploads.md +746 -0
- package/claude/commands/how-to/implement-search.md +1001 -0
- package/claude/commands/how-to/install-plugins.md +352 -0
- package/claude/commands/how-to/manage-test-coverage.md +984 -0
- package/claude/commands/how-to/run-tests.md +400 -0
- package/claude/commands/how-to/set-app-languages.md +601 -0
- package/claude/commands/how-to/set-plans-and-permissions.md +575 -0
- package/claude/commands/how-to/set-scheduled-actions.md +527 -0
- package/claude/commands/how-to/set-user-roles-and-permissions.md +550 -0
- package/claude/commands/how-to/setup-authentication.md +388 -0
- package/claude/commands/how-to/setup-claude-code.md +440 -0
- package/claude/commands/how-to/setup-database.md +274 -0
- package/claude/commands/how-to/setup-email-providers.md +598 -0
- package/claude/commands/how-to/setup-mobile-dev.md +627 -0
- package/claude/commands/how-to/start.md +500 -0
- package/claude/commands/how-to/use-devtools.md +639 -0
- package/claude/commands/how-to/use-superadmin.md +622 -0
- package/claude/commands/session/README.md +193 -0
- package/claude/commands/session/block-create.md +190 -0
- package/claude/commands/session/block-list.md +203 -0
- package/claude/commands/session/block-update.md +192 -0
- package/claude/commands/session/block-validate.md +218 -0
- package/claude/commands/session/changelog.md +115 -0
- package/claude/commands/session/close.md +225 -0
- package/claude/commands/session/commit.md +174 -0
- package/claude/commands/session/db-entity.md +206 -0
- package/claude/commands/session/db-fix.md +212 -0
- package/claude/commands/session/db-sample.md +206 -0
- package/claude/commands/session/demo.md +178 -0
- package/claude/commands/session/doc-bdd.md +207 -0
- package/claude/commands/session/doc-feature.md +218 -0
- package/claude/commands/session/doc-read.md +225 -0
- package/claude/commands/session/execute.md +204 -0
- package/claude/commands/session/explain.md +202 -0
- package/claude/commands/session/fix-bug.md +210 -0
- package/claude/commands/session/fix-build.md +182 -0
- package/claude/commands/session/fix-test.md +189 -0
- package/claude/commands/session/pending.md +232 -0
- package/claude/commands/session/refine.md +188 -0
- package/claude/commands/session/resume.md +192 -0
- package/claude/commands/session/review.md +192 -0
- package/claude/commands/session/scope-change.md +181 -0
- package/claude/commands/session/start-blocks.md +347 -0
- package/claude/commands/session/start.md +604 -0
- package/claude/commands/session/status.md +169 -0
- package/claude/commands/session/test-fix.md +221 -0
- package/claude/commands/session/test-run.md +203 -0
- package/claude/commands/session/test-write.md +242 -0
- package/claude/commands/session/validate.md +162 -0
- package/claude/config/context.json +40 -0
- package/claude/config/github.json +69 -0
- package/claude/config/github.schema.json +106 -0
- package/claude/config/team.json +46 -0
- package/claude/config/team.schema.json +106 -0
- package/claude/config/workspace.json +43 -0
- package/claude/config/workspace.schema.json +75 -0
- package/claude/skills/README.md +228 -0
- package/claude/skills/accessibility/SKILL.md +573 -0
- package/claude/skills/api-bypass-layers/SKILL.md +550 -0
- package/claude/skills/asana-integration/SKILL.md +499 -0
- package/claude/skills/better-auth/SKILL.md +666 -0
- package/claude/skills/billing-subscriptions/SKILL.md +660 -0
- package/claude/skills/block-decision-matrix/SKILL.md +359 -0
- package/claude/skills/clickup-integration/SKILL.md +434 -0
- package/claude/skills/core-theme-responsibilities/SKILL.md +485 -0
- package/claude/skills/create-plugin/SKILL.md +425 -0
- package/claude/skills/create-theme/SKILL.md +331 -0
- package/claude/skills/cypress-api/SKILL.md +511 -0
- package/claude/skills/cypress-api/scripts/generate-api-controller.py +329 -0
- package/claude/skills/cypress-api/scripts/generate-api-test.py +930 -0
- package/claude/skills/cypress-e2e/SKILL.md +526 -0
- package/claude/skills/cypress-e2e/scripts/extract-selectors.py +383 -0
- package/claude/skills/cypress-e2e/scripts/generate-uat-test.py +788 -0
- package/claude/skills/cypress-selectors/SKILL.md +309 -0
- package/claude/skills/cypress-selectors/scripts/extract-missing.py +243 -0
- package/claude/skills/cypress-selectors/scripts/generate-block-selectors.py +283 -0
- package/claude/skills/cypress-selectors/scripts/validate-selectors.py +145 -0
- package/claude/skills/database-migrations/SKILL.md +335 -0
- package/claude/skills/database-migrations/scripts/generate-sample-data.py +284 -0
- package/claude/skills/database-migrations/scripts/validate-migration.py +323 -0
- package/claude/skills/design-system/SKILL.md +682 -0
- package/claude/skills/documentation/SKILL.md +540 -0
- package/claude/skills/entity-api/SKILL.md +482 -0
- package/claude/skills/entity-system/SKILL.md +635 -0
- package/claude/skills/entity-system/scripts/generate-child-migration.py +298 -0
- package/claude/skills/entity-system/scripts/generate-metas-migration.py +233 -0
- package/claude/skills/entity-system/scripts/generate-migration.py +382 -0
- package/claude/skills/entity-system/scripts/generate-sample-data.py +418 -0
- package/claude/skills/entity-system/scripts/scaffold-entity.py +661 -0
- package/claude/skills/github/SKILL.md +467 -0
- package/claude/skills/i18n-nextintl/SKILL.md +302 -0
- package/claude/skills/i18n-nextintl/scripts/add-translation.py +243 -0
- package/claude/skills/i18n-nextintl/scripts/extract-hardcoded.py +246 -0
- package/claude/skills/i18n-nextintl/scripts/validate-translations.py +260 -0
- package/claude/skills/impact-analysis/SKILL.md +203 -0
- package/claude/skills/jest-unit/SKILL.md +306 -0
- package/claude/skills/jest-unit/references/component-testing.md +371 -0
- package/claude/skills/jest-unit/references/mocking-patterns.md +380 -0
- package/claude/skills/jest-unit/references/service-hook-testing.md +454 -0
- package/claude/skills/jira-integration/SKILL.md +539 -0
- package/claude/skills/media-library/SKILL.md +743 -0
- package/claude/skills/mock-analysis/SKILL.md +276 -0
- package/claude/skills/monorepo-architecture/SKILL.md +162 -0
- package/claude/skills/nextjs-api-development/SKILL.md +364 -0
- package/claude/skills/nextjs-api-development/scripts/generate-crud-tests.py +456 -0
- package/claude/skills/nextjs-api-development/scripts/scaffold-endpoint.py +481 -0
- package/claude/skills/nextjs-api-development/scripts/validate-api.py +283 -0
- package/claude/skills/notion-integration/SKILL.md +641 -0
- package/claude/skills/npm-development-workflow/SKILL.md +480 -0
- package/claude/skills/page-builder-blocks/SKILL.md +530 -0
- package/claude/skills/page-builder-blocks/scripts/scaffold-block.py +444 -0
- package/claude/skills/permissions-system/SKILL.md +619 -0
- package/claude/skills/plugins/SKILL.md +340 -0
- package/claude/skills/plugins/references/plugin-templates.md +414 -0
- package/claude/skills/plugins/references/plugin-testing.md +353 -0
- package/claude/skills/plugins/references/plugin-types.md +198 -0
- package/claude/skills/plugins/scripts/scaffold-plugin.py +443 -0
- package/claude/skills/pom-patterns/SKILL.md +452 -0
- package/claude/skills/pom-patterns/scripts/generate-pom.py +392 -0
- package/claude/skills/rate-limiting/SKILL.md +342 -0
- package/claude/skills/react-best-practices/AGENTS.md +2410 -0
- package/claude/skills/react-best-practices/README.md +123 -0
- package/claude/skills/react-best-practices/SKILL.md +125 -0
- package/claude/skills/react-best-practices/metadata.json +15 -0
- package/claude/skills/react-best-practices/rules/_sections.md +46 -0
- package/claude/skills/react-best-practices/rules/_template.md +28 -0
- package/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/claude/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/claude/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/claude/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/claude/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/claude/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/claude/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/claude/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/claude/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/claude/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/claude/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/claude/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/claude/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/claude/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/claude/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/claude/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/claude/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/claude/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/claude/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/claude/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/claude/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/claude/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/claude/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/claude/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/claude/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/claude/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/claude/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/claude/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/claude/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/claude/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/claude/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/claude/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/claude/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/claude/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/claude/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/claude/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/claude/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/claude/skills/react-patterns/SKILL.md +688 -0
- package/claude/skills/registry-system/SKILL.md +331 -0
- package/claude/skills/scheduled-actions/SKILL.md +671 -0
- package/claude/skills/scope-enforcement/SKILL.md +542 -0
- package/claude/skills/scope-enforcement/scripts/validate-scope.py +357 -0
- package/claude/skills/server-actions/SKILL.md +493 -0
- package/claude/skills/service-layer/SKILL.md +587 -0
- package/claude/skills/session-management/SKILL.md +266 -0
- package/claude/skills/session-management/scripts/create-session.py +166 -0
- package/claude/skills/session-management/scripts/iteration-close.sh +105 -0
- package/claude/skills/session-management/scripts/iteration-init.sh +180 -0
- package/claude/skills/session-management/scripts/session-archive.sh +87 -0
- package/claude/skills/session-management/scripts/session-close.sh +133 -0
- package/claude/skills/session-management/scripts/session-init.sh +225 -0
- package/claude/skills/session-management/scripts/session-list.sh +163 -0
- package/claude/skills/session-management/scripts/split-plan.sh +116 -0
- package/claude/skills/shadcn-components/SKILL.md +586 -0
- package/claude/skills/shadcn-theming/SKILL.md +446 -0
- package/claude/skills/suspense-loading/SKILL.md +280 -0
- package/claude/skills/tailwind-theming/SKILL.md +507 -0
- package/claude/skills/tanstack-query/SKILL.md +608 -0
- package/claude/skills/test-coverage/SKILL.md +239 -0
- package/claude/skills/web-design-guidelines/SKILL.md +39 -0
- package/claude/skills/zod-validation/SKILL.md +537 -0
- package/claude/templates/blocks/progress.md +86 -0
- package/claude/templates/iteration/changes.md +61 -0
- package/claude/templates/iteration/progress.md +55 -0
- package/claude/templates/log.md +31 -0
- package/claude/templates/story/context.md +77 -0
- package/claude/templates/story/pendings.md +37 -0
- package/claude/templates/story/plan.md +299 -0
- package/claude/templates/story/requirements.md +109 -0
- package/claude/templates/story/scope.json +10 -0
- package/claude/templates/story/tests.md +91 -0
- package/claude/templates/task/progress.md +58 -0
- package/claude/templates/task/requirements.md +54 -0
- package/claude/workflows/README.md +154 -0
- package/claude/workflows/blocks.md +614 -0
- package/claude/workflows/story.md +1207 -0
- package/claude/workflows/task.md +927 -0
- package/claude/workflows/tweak.md +527 -0
- package/cursor/.gitkeep +0 -0
- package/package.json +35 -0
- package/scripts/postinstall.mjs +198 -0
- package/scripts/setup.mjs +282 -0
- package/scripts/sync.mjs +209 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generate Child Entity Migration Script
|
|
4
|
+
|
|
5
|
+
Generates SQL migration for child entity table with parent reference.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python generate-child-migration.py --parent PARENT --child CHILD [--theme THEME]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--parent PARENT Parent entity name (kebab-case, e.g., 'orders')
|
|
12
|
+
--child CHILD Child entity name (kebab-case, e.g., 'items')
|
|
13
|
+
--theme THEME Theme name (default: from NEXT_PUBLIC_ACTIVE_THEME or 'default')
|
|
14
|
+
--output FILE Output file path
|
|
15
|
+
--fields FIELDS Comma-separated field:type pairs (e.g., 'name:text,quantity:number')
|
|
16
|
+
--with-rls Include RLS policies (default: true)
|
|
17
|
+
--no-rls Exclude RLS policies
|
|
18
|
+
--dry-run Print SQL without writing file
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import argparse
|
|
24
|
+
import re
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from typing import List, Dict, Tuple
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_active_theme() -> str:
|
|
31
|
+
"""Get active theme from environment or default."""
|
|
32
|
+
return os.environ.get('NEXT_PUBLIC_ACTIVE_THEME', 'default')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_snake_case(name: str) -> str:
|
|
36
|
+
"""Convert kebab-case to snake_case."""
|
|
37
|
+
return name.replace('-', '_')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_camel_case(name: str) -> str:
|
|
41
|
+
"""Convert kebab-case to camelCase."""
|
|
42
|
+
components = name.split('-')
|
|
43
|
+
return components[0] + ''.join(x.title() for x in components[1:])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Field type to SQL type mapping
|
|
47
|
+
FIELD_TYPE_MAP = {
|
|
48
|
+
'text': 'VARCHAR(255)',
|
|
49
|
+
'textarea': 'TEXT',
|
|
50
|
+
'number': 'DECIMAL(10, 2)',
|
|
51
|
+
'integer': 'INTEGER',
|
|
52
|
+
'boolean': 'BOOLEAN DEFAULT FALSE',
|
|
53
|
+
'date': 'DATE',
|
|
54
|
+
'datetime': 'TIMESTAMPTZ',
|
|
55
|
+
'email': 'VARCHAR(255)',
|
|
56
|
+
'url': 'TEXT',
|
|
57
|
+
'json': 'JSONB DEFAULT \'{}\'::jsonb',
|
|
58
|
+
'select': 'VARCHAR(100)',
|
|
59
|
+
'uuid': 'TEXT',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_fields(fields_str: str) -> List[Dict]:
|
|
64
|
+
"""Parse comma-separated field:type pairs."""
|
|
65
|
+
if not fields_str:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
fields = []
|
|
69
|
+
for field_def in fields_str.split(','):
|
|
70
|
+
parts = field_def.strip().split(':')
|
|
71
|
+
if len(parts) >= 2:
|
|
72
|
+
name = parts[0].strip()
|
|
73
|
+
field_type = parts[1].strip()
|
|
74
|
+
required = len(parts) > 2 and parts[2].strip().lower() == 'required'
|
|
75
|
+
fields.append({
|
|
76
|
+
'name': name,
|
|
77
|
+
'type': field_type,
|
|
78
|
+
'required': required
|
|
79
|
+
})
|
|
80
|
+
return fields
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def to_pascal_case(name: str) -> str:
|
|
84
|
+
"""Convert kebab-case to PascalCase."""
|
|
85
|
+
return ''.join(x.title() for x in name.split('-'))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def generate_child_migration(
|
|
89
|
+
parent_slug: str,
|
|
90
|
+
child_slug: str,
|
|
91
|
+
fields: List[Dict],
|
|
92
|
+
with_rls: bool = True
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Generate child entity table migration SQL matching project conventions."""
|
|
95
|
+
|
|
96
|
+
parent_table = to_snake_case(parent_slug)
|
|
97
|
+
child_table = f"{parent_table}_{to_snake_case(child_slug)}"
|
|
98
|
+
parent_pascal = to_pascal_case(parent_slug)
|
|
99
|
+
child_pascal = to_pascal_case(child_slug)
|
|
100
|
+
date_str = datetime.now().strftime('%Y-%m-%d')
|
|
101
|
+
|
|
102
|
+
# Build column definitions with proper alignment
|
|
103
|
+
columns = [
|
|
104
|
+
' -- Primary Key',
|
|
105
|
+
' id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,',
|
|
106
|
+
'',
|
|
107
|
+
' -- Parent Reference',
|
|
108
|
+
f' "parentId" TEXT NOT NULL REFERENCES public."{parent_table}"(id) ON DELETE CASCADE,',
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# Add custom fields if any
|
|
112
|
+
if fields:
|
|
113
|
+
columns.append('')
|
|
114
|
+
columns.append(' -- Entity-specific fields')
|
|
115
|
+
for field in fields:
|
|
116
|
+
name = field['name']
|
|
117
|
+
field_type = field['type']
|
|
118
|
+
required = field.get('required', False)
|
|
119
|
+
|
|
120
|
+
sql_type = FIELD_TYPE_MAP.get(field_type, 'TEXT')
|
|
121
|
+
not_null = ' NOT NULL' if required else ''
|
|
122
|
+
# Pad the name for alignment
|
|
123
|
+
padded_name = f'"{name}"'.ljust(14) if name[0].isupper() or '-' in name else name.ljust(14)
|
|
124
|
+
columns.append(f' {padded_name} {sql_type}{not_null},')
|
|
125
|
+
|
|
126
|
+
# System fields
|
|
127
|
+
columns.append('')
|
|
128
|
+
columns.append(' -- System fields')
|
|
129
|
+
columns.append(' "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),')
|
|
130
|
+
columns.append(' "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()')
|
|
131
|
+
|
|
132
|
+
columns_sql = '\n'.join(columns)
|
|
133
|
+
|
|
134
|
+
sql = f'''-- Migration: 003_{child_table}.sql
|
|
135
|
+
-- Description: {parent_pascal} {child_pascal} child entity (table, indexes, RLS)
|
|
136
|
+
-- Date: {date_str}
|
|
137
|
+
|
|
138
|
+
-- ============================================
|
|
139
|
+
-- TABLE
|
|
140
|
+
-- ============================================
|
|
141
|
+
CREATE TABLE IF NOT EXISTS public."{child_table}" (
|
|
142
|
+
{columns_sql}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
COMMENT ON TABLE public."{child_table}" IS 'Child entity: {child_slug} for {parent_slug}';
|
|
146
|
+
COMMENT ON COLUMN public."{child_table}"."parentId" IS 'Reference to parent {parent_slug}';
|
|
147
|
+
|
|
148
|
+
-- ============================================
|
|
149
|
+
-- TRIGGER updatedAt
|
|
150
|
+
-- ============================================
|
|
151
|
+
DROP TRIGGER IF EXISTS {child_table}_set_updated_at ON public."{child_table}";
|
|
152
|
+
CREATE TRIGGER {child_table}_set_updated_at
|
|
153
|
+
BEFORE UPDATE ON public."{child_table}"
|
|
154
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
155
|
+
|
|
156
|
+
-- ============================================
|
|
157
|
+
-- INDEXES
|
|
158
|
+
-- ============================================
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_{child_table}_parent_id ON public."{child_table}"("parentId");
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_{child_table}_created_at ON public."{child_table}"("createdAt" DESC);
|
|
161
|
+
'''
|
|
162
|
+
|
|
163
|
+
if with_rls:
|
|
164
|
+
sql += f'''
|
|
165
|
+
-- ============================================
|
|
166
|
+
-- RLS
|
|
167
|
+
-- ============================================
|
|
168
|
+
ALTER TABLE public."{child_table}" ENABLE ROW LEVEL SECURITY;
|
|
169
|
+
|
|
170
|
+
-- Cleanup existing policies
|
|
171
|
+
DROP POLICY IF EXISTS "{parent_pascal} {child_pascal} team can do all" ON public."{child_table}";
|
|
172
|
+
|
|
173
|
+
-- ============================
|
|
174
|
+
-- RLS: TEAM ISOLATION VIA PARENT
|
|
175
|
+
-- ============================
|
|
176
|
+
-- Hereda el aislamiento del parent via teamId
|
|
177
|
+
CREATE POLICY "{parent_pascal} {child_pascal} team can do all"
|
|
178
|
+
ON public."{child_table}"
|
|
179
|
+
FOR ALL TO authenticated
|
|
180
|
+
USING (
|
|
181
|
+
-- Superadmin bypass
|
|
182
|
+
public.is_superadmin()
|
|
183
|
+
OR
|
|
184
|
+
-- Team isolation via parent
|
|
185
|
+
EXISTS (
|
|
186
|
+
SELECT 1 FROM public."{parent_table}" t
|
|
187
|
+
WHERE t.id = "parentId"
|
|
188
|
+
AND t."teamId" = ANY(public.get_user_team_ids())
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
WITH CHECK (
|
|
192
|
+
public.is_superadmin()
|
|
193
|
+
OR
|
|
194
|
+
EXISTS (
|
|
195
|
+
SELECT 1 FROM public."{parent_table}" t
|
|
196
|
+
WHERE t.id = "parentId"
|
|
197
|
+
AND t."teamId" = ANY(public.get_user_team_ids())
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
'''
|
|
201
|
+
|
|
202
|
+
return sql
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def find_next_migration_number(migrations_dir: Path) -> int:
|
|
206
|
+
"""Find the next available migration number."""
|
|
207
|
+
if not migrations_dir.exists():
|
|
208
|
+
return 1
|
|
209
|
+
|
|
210
|
+
existing = list(migrations_dir.glob('*.sql'))
|
|
211
|
+
if not existing:
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
numbers = []
|
|
215
|
+
for f in existing:
|
|
216
|
+
try:
|
|
217
|
+
num = int(f.name.split('_')[0])
|
|
218
|
+
numbers.append(num)
|
|
219
|
+
except (ValueError, IndexError):
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
return max(numbers, default=0) + 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main():
|
|
226
|
+
parser = argparse.ArgumentParser(description='Generate child entity migration')
|
|
227
|
+
parser.add_argument('--parent', required=True, help='Parent entity name (kebab-case)')
|
|
228
|
+
parser.add_argument('--child', required=True, help='Child entity name (kebab-case)')
|
|
229
|
+
parser.add_argument('--theme', default=None, help='Theme name')
|
|
230
|
+
parser.add_argument('--output', help='Output file path')
|
|
231
|
+
parser.add_argument('--fields', help='Comma-separated field:type pairs')
|
|
232
|
+
parser.add_argument('--with-rls', action='store_true', default=True)
|
|
233
|
+
parser.add_argument('--no-rls', action='store_true', help='Exclude RLS policies')
|
|
234
|
+
parser.add_argument('--dry-run', action='store_true', help='Print SQL without writing')
|
|
235
|
+
|
|
236
|
+
args = parser.parse_args()
|
|
237
|
+
|
|
238
|
+
theme = args.theme or get_active_theme()
|
|
239
|
+
parent_slug = args.parent.lower()
|
|
240
|
+
child_slug = args.child.lower()
|
|
241
|
+
with_rls = not args.no_rls
|
|
242
|
+
|
|
243
|
+
# Parse fields
|
|
244
|
+
fields = parse_fields(args.fields) if args.fields else []
|
|
245
|
+
|
|
246
|
+
print(f"\n{'='*60}")
|
|
247
|
+
print(f"Generating child entity migration")
|
|
248
|
+
print(f"Parent: {parent_slug}")
|
|
249
|
+
print(f"Child: {child_slug}")
|
|
250
|
+
print(f"Theme: {theme}")
|
|
251
|
+
print(f"With RLS: {with_rls}")
|
|
252
|
+
print(f"Fields: {len(fields)}")
|
|
253
|
+
print(f"{'='*60}")
|
|
254
|
+
|
|
255
|
+
# Generate SQL
|
|
256
|
+
sql = generate_child_migration(parent_slug, child_slug, fields, with_rls)
|
|
257
|
+
|
|
258
|
+
if args.dry_run:
|
|
259
|
+
print("\n" + sql)
|
|
260
|
+
print(f"\n{'='*60}")
|
|
261
|
+
print("DRY RUN - No file written")
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
# Determine output path
|
|
265
|
+
if args.output:
|
|
266
|
+
output_path = Path(args.output)
|
|
267
|
+
else:
|
|
268
|
+
# Auto-generate path in parent entity's migrations folder
|
|
269
|
+
entity_dir = Path(f'contents/themes/{theme}/entities/{parent_slug}')
|
|
270
|
+
migrations_dir = entity_dir / 'migrations'
|
|
271
|
+
|
|
272
|
+
if not entity_dir.exists():
|
|
273
|
+
print(f"\nError: Parent entity directory not found: {entity_dir}")
|
|
274
|
+
print("Please create the parent entity first using scaffold-entity.py")
|
|
275
|
+
return 1
|
|
276
|
+
|
|
277
|
+
migrations_dir.mkdir(exist_ok=True)
|
|
278
|
+
|
|
279
|
+
next_num = find_next_migration_number(migrations_dir)
|
|
280
|
+
parent_table = to_snake_case(parent_slug)
|
|
281
|
+
child_table = to_snake_case(child_slug)
|
|
282
|
+
output_path = migrations_dir / f'{next_num:03d}_{parent_table}_{child_table}.sql'
|
|
283
|
+
|
|
284
|
+
# Write file
|
|
285
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
286
|
+
f.write(sql)
|
|
287
|
+
|
|
288
|
+
print(f"\nMigration written to: {output_path}")
|
|
289
|
+
print(f"\nNext steps:")
|
|
290
|
+
print(f" 1. Add fields to the migration if not specified via --fields")
|
|
291
|
+
print(f" 2. Add childEntities config to {parent_slug}.config.ts")
|
|
292
|
+
print(f" 3. Run: pnpm db:migrate")
|
|
293
|
+
|
|
294
|
+
return 0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == '__main__':
|
|
298
|
+
sys.exit(main())
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generate Metas Migration Script
|
|
4
|
+
|
|
5
|
+
Generates SQL migration for entity metadata table.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python generate-metas-migration.py --entity ENTITY_NAME [--theme THEME] [--output FILE]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--entity ENTITY_NAME Name of the entity (kebab-case, e.g., 'products', 'blog-posts')
|
|
12
|
+
--theme THEME Theme name (default: from NEXT_PUBLIC_ACTIVE_THEME or 'default')
|
|
13
|
+
--output FILE Output file (default: auto-generate path)
|
|
14
|
+
--with-rls Include RLS policies (default: true)
|
|
15
|
+
--dry-run Print SQL without writing file
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import argparse
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_active_theme() -> str:
|
|
26
|
+
"""Get active theme from environment or default."""
|
|
27
|
+
return os.environ.get('NEXT_PUBLIC_ACTIVE_THEME', 'default')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_camel_case(name: str) -> str:
|
|
31
|
+
"""Convert kebab-case to camelCase."""
|
|
32
|
+
components = name.split('-')
|
|
33
|
+
return components[0] + ''.join(x.title() for x in components[1:])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def to_singular(name: str) -> str:
|
|
37
|
+
"""Simple pluralization - remove trailing 's' for common cases."""
|
|
38
|
+
if name.endswith('ies'):
|
|
39
|
+
return name[:-3] + 'y'
|
|
40
|
+
elif name.endswith('es') and not name.endswith('ses'):
|
|
41
|
+
return name[:-2]
|
|
42
|
+
elif name.endswith('s') and not name.endswith('ss'):
|
|
43
|
+
return name[:-1]
|
|
44
|
+
return name
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_pascal_case(name: str) -> str:
|
|
48
|
+
"""Convert kebab-case to PascalCase."""
|
|
49
|
+
return ''.join(x.title() for x in name.split('-'))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def generate_metas_migration(entity_slug: str, with_rls: bool = True) -> str:
|
|
53
|
+
"""Generate metadata table migration SQL matching project conventions."""
|
|
54
|
+
|
|
55
|
+
# Entity naming
|
|
56
|
+
table_name = entity_slug.replace('-', '_')
|
|
57
|
+
singular = to_singular(entity_slug).replace('-', '_')
|
|
58
|
+
pascal = to_pascal_case(entity_slug)
|
|
59
|
+
date_str = datetime.now().strftime('%Y-%m-%d')
|
|
60
|
+
|
|
61
|
+
sql = f'''-- Migration: 002_{table_name}_metas.sql
|
|
62
|
+
-- Description: {pascal} metas (table, indexes, RLS)
|
|
63
|
+
-- Date: {date_str}
|
|
64
|
+
|
|
65
|
+
-- ============================================
|
|
66
|
+
-- TABLE
|
|
67
|
+
-- ============================================
|
|
68
|
+
CREATE TABLE IF NOT EXISTS public."{table_name}_metas" (
|
|
69
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
70
|
+
"entityId" TEXT NOT NULL REFERENCES public."{table_name}"(id) ON DELETE CASCADE,
|
|
71
|
+
"metaKey" TEXT NOT NULL,
|
|
72
|
+
"metaValue" JSONB NOT NULL DEFAULT '{{}}'::jsonb,
|
|
73
|
+
"dataType" TEXT DEFAULT 'json',
|
|
74
|
+
"isPublic" BOOLEAN NOT NULL DEFAULT false,
|
|
75
|
+
"isSearchable" BOOLEAN NOT NULL DEFAULT false,
|
|
76
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
77
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
78
|
+
CONSTRAINT {table_name}_metas_unique_key UNIQUE ("entityId", "metaKey")
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
COMMENT ON TABLE public."{table_name}_metas" IS 'Key-value metadata for {table_name}';
|
|
82
|
+
COMMENT ON COLUMN public."{table_name}_metas"."entityId" IS 'Reference to parent {singular}';
|
|
83
|
+
COMMENT ON COLUMN public."{table_name}_metas"."metaKey" IS 'Metadata key identifier';
|
|
84
|
+
COMMENT ON COLUMN public."{table_name}_metas"."metaValue" IS 'Metadata value in JSONB format';
|
|
85
|
+
COMMENT ON COLUMN public."{table_name}_metas"."dataType" IS 'Type hint: json, string, number, boolean';
|
|
86
|
+
COMMENT ON COLUMN public."{table_name}_metas"."isPublic" IS 'Whether this metadata is publicly readable';
|
|
87
|
+
COMMENT ON COLUMN public."{table_name}_metas"."isSearchable" IS 'Whether this metadata is searchable';
|
|
88
|
+
|
|
89
|
+
-- ============================================
|
|
90
|
+
-- TRIGGER updatedAt
|
|
91
|
+
-- ============================================
|
|
92
|
+
DROP TRIGGER IF EXISTS {table_name}_metas_set_updated_at ON public."{table_name}_metas";
|
|
93
|
+
CREATE TRIGGER {table_name}_metas_set_updated_at
|
|
94
|
+
BEFORE UPDATE ON public."{table_name}_metas"
|
|
95
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
96
|
+
|
|
97
|
+
-- ============================================
|
|
98
|
+
-- INDEXES
|
|
99
|
+
-- ============================================
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_{table_name}_metas_entity_id ON public."{table_name}_metas"("entityId");
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_{table_name}_metas_key ON public."{table_name}_metas"("metaKey");
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_{table_name}_metas_is_public ON public."{table_name}_metas"("isPublic") WHERE "isPublic" = true;
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_{table_name}_metas_searchable ON public."{table_name}_metas"("isSearchable") WHERE "isSearchable" = true;
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_{table_name}_metas_value_gin ON public."{table_name}_metas" USING GIN ("metaValue");
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_{table_name}_metas_value_ops ON public."{table_name}_metas" USING GIN ("metaValue" jsonb_path_ops);
|
|
106
|
+
'''
|
|
107
|
+
|
|
108
|
+
if with_rls:
|
|
109
|
+
sql += f'''
|
|
110
|
+
-- ============================================
|
|
111
|
+
-- RLS
|
|
112
|
+
-- ============================================
|
|
113
|
+
ALTER TABLE public."{table_name}_metas" ENABLE ROW LEVEL SECURITY;
|
|
114
|
+
|
|
115
|
+
-- Cleanup existing policies
|
|
116
|
+
DROP POLICY IF EXISTS "{pascal} metas team can do all" ON public."{table_name}_metas";
|
|
117
|
+
|
|
118
|
+
-- ============================
|
|
119
|
+
-- RLS: TEAM ISOLATION VIA PARENT
|
|
120
|
+
-- ============================
|
|
121
|
+
-- Hereda el aislamiento del parent via teamId
|
|
122
|
+
CREATE POLICY "{pascal} metas team can do all"
|
|
123
|
+
ON public."{table_name}_metas"
|
|
124
|
+
FOR ALL TO authenticated
|
|
125
|
+
USING (
|
|
126
|
+
-- Superadmin bypass
|
|
127
|
+
public.is_superadmin()
|
|
128
|
+
OR
|
|
129
|
+
-- Team isolation via parent
|
|
130
|
+
EXISTS (
|
|
131
|
+
SELECT 1 FROM public."{table_name}" t
|
|
132
|
+
WHERE t.id = "entityId"
|
|
133
|
+
AND t."teamId" = ANY(public.get_user_team_ids())
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
WITH CHECK (
|
|
137
|
+
public.is_superadmin()
|
|
138
|
+
OR
|
|
139
|
+
EXISTS (
|
|
140
|
+
SELECT 1 FROM public."{table_name}" t
|
|
141
|
+
WHERE t.id = "entityId"
|
|
142
|
+
AND t."teamId" = ANY(public.get_user_team_ids())
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
'''
|
|
146
|
+
|
|
147
|
+
return sql
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def find_next_migration_number(migrations_dir: Path) -> int:
|
|
151
|
+
"""Find the next available migration number."""
|
|
152
|
+
if not migrations_dir.exists():
|
|
153
|
+
return 1
|
|
154
|
+
|
|
155
|
+
existing = list(migrations_dir.glob('*.sql'))
|
|
156
|
+
if not existing:
|
|
157
|
+
return 1
|
|
158
|
+
|
|
159
|
+
numbers = []
|
|
160
|
+
for f in existing:
|
|
161
|
+
try:
|
|
162
|
+
num = int(f.name.split('_')[0])
|
|
163
|
+
numbers.append(num)
|
|
164
|
+
except (ValueError, IndexError):
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
return max(numbers, default=0) + 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
parser = argparse.ArgumentParser(description='Generate entity metas migration')
|
|
172
|
+
parser.add_argument('--entity', required=True, help='Entity name (kebab-case)')
|
|
173
|
+
parser.add_argument('--theme', default=None, help='Theme name')
|
|
174
|
+
parser.add_argument('--output', help='Output file path')
|
|
175
|
+
parser.add_argument('--with-rls', action='store_true', default=True, help='Include RLS policies')
|
|
176
|
+
parser.add_argument('--no-rls', action='store_true', help='Exclude RLS policies')
|
|
177
|
+
parser.add_argument('--dry-run', action='store_true', help='Print SQL without writing')
|
|
178
|
+
|
|
179
|
+
args = parser.parse_args()
|
|
180
|
+
|
|
181
|
+
theme = args.theme or get_active_theme()
|
|
182
|
+
entity_slug = args.entity.lower()
|
|
183
|
+
with_rls = not args.no_rls
|
|
184
|
+
|
|
185
|
+
print(f"\n{'='*60}")
|
|
186
|
+
print(f"Generating metas migration for: {entity_slug}")
|
|
187
|
+
print(f"Theme: {theme}")
|
|
188
|
+
print(f"With RLS: {with_rls}")
|
|
189
|
+
print(f"{'='*60}")
|
|
190
|
+
|
|
191
|
+
# Generate SQL
|
|
192
|
+
sql = generate_metas_migration(entity_slug, with_rls)
|
|
193
|
+
|
|
194
|
+
if args.dry_run:
|
|
195
|
+
print("\n" + sql)
|
|
196
|
+
print(f"\n{'='*60}")
|
|
197
|
+
print("DRY RUN - No file written")
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
# Determine output path
|
|
201
|
+
if args.output:
|
|
202
|
+
output_path = Path(args.output)
|
|
203
|
+
else:
|
|
204
|
+
# Auto-generate path in entity's migrations folder
|
|
205
|
+
entity_dir = Path(f'contents/themes/{theme}/entities/{entity_slug}')
|
|
206
|
+
migrations_dir = entity_dir / 'migrations'
|
|
207
|
+
|
|
208
|
+
if not entity_dir.exists():
|
|
209
|
+
print(f"\nError: Entity directory not found: {entity_dir}")
|
|
210
|
+
print("Please create the entity first using scaffold-entity.py")
|
|
211
|
+
return 1
|
|
212
|
+
|
|
213
|
+
migrations_dir.mkdir(exist_ok=True)
|
|
214
|
+
|
|
215
|
+
next_num = find_next_migration_number(migrations_dir)
|
|
216
|
+
table_name = entity_slug.replace('-', '_')
|
|
217
|
+
output_path = migrations_dir / f'{next_num:03d}_{table_name}_metas.sql'
|
|
218
|
+
|
|
219
|
+
# Write file
|
|
220
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
221
|
+
f.write(sql)
|
|
222
|
+
|
|
223
|
+
print(f"\nMigration written to: {output_path}")
|
|
224
|
+
print(f"\nNext steps:")
|
|
225
|
+
print(f" 1. Review the generated migration")
|
|
226
|
+
print(f" 2. Run: pnpm db:migrate")
|
|
227
|
+
print(f" 3. Update entity config: access.metadata = true")
|
|
228
|
+
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == '__main__':
|
|
233
|
+
sys.exit(main())
|