laranex-schema-viewer 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. laranex_schema_viewer-0.1.0/.gh-page/.vitepress/config.mts +71 -0
  2. laranex_schema_viewer-0.1.0/.gh-page/.vitepress/theme/LiveDemo.vue +24 -0
  3. laranex_schema_viewer-0.1.0/.gh-page/.vitepress/theme/PackageIntroduction.vue +28 -0
  4. laranex_schema_viewer-0.1.0/.gh-page/.vitepress/theme/custom.css +82 -0
  5. laranex_schema_viewer-0.1.0/.gh-page/.vitepress/theme/index.ts +14 -0
  6. laranex_schema_viewer-0.1.0/.gh-page/demo/schema.mjs +76 -0
  7. laranex_schema_viewer-0.1.0/.gh-page/package-lock.json +2650 -0
  8. laranex_schema_viewer-0.1.0/.gh-page/package.json +20 -0
  9. laranex_schema_viewer-0.1.0/.gh-page/public/.nojekyll +0 -0
  10. laranex_schema_viewer-0.1.0/.gh-page/public/logo.svg +1 -0
  11. laranex_schema_viewer-0.1.0/.gh-page/scripts/check-build.mjs +40 -0
  12. laranex_schema_viewer-0.1.0/.gh-page/scripts/prepare-demo.mjs +29 -0
  13. laranex_schema_viewer-0.1.0/.gh-page/tests/demo.test.mjs +41 -0
  14. laranex_schema_viewer-0.1.0/.github/workflows/deploy-docs.yml +69 -0
  15. laranex_schema_viewer-0.1.0/.github/workflows/publish.yml +45 -0
  16. laranex_schema_viewer-0.1.0/.github/workflows/tests.yml +37 -0
  17. laranex_schema_viewer-0.1.0/.gitignore +31 -0
  18. laranex_schema_viewer-0.1.0/AGENTS.md +74 -0
  19. laranex_schema_viewer-0.1.0/CLAUDE.md +2 -0
  20. laranex_schema_viewer-0.1.0/LICENSE +9 -0
  21. laranex_schema_viewer-0.1.0/NOTICE +10 -0
  22. laranex_schema_viewer-0.1.0/PKG-INFO +100 -0
  23. laranex_schema_viewer-0.1.0/README.md +54 -0
  24. laranex_schema_viewer-0.1.0/SECURITY.md +20 -0
  25. laranex_schema_viewer-0.1.0/docs/agent-skills-validation.md +41 -0
  26. laranex_schema_viewer-0.1.0/docs/agent-skills.md +90 -0
  27. laranex_schema_viewer-0.1.0/docs/architecture.md +39 -0
  28. laranex_schema_viewer-0.1.0/docs/cli.md +47 -0
  29. laranex_schema_viewer-0.1.0/docs/demo.md +19 -0
  30. laranex_schema_viewer-0.1.0/docs/discovery.md +36 -0
  31. laranex_schema_viewer-0.1.0/docs/index.md +21 -0
  32. laranex_schema_viewer-0.1.0/docs/installation.md +41 -0
  33. laranex_schema_viewer-0.1.0/docs/introduction.md +33 -0
  34. laranex_schema_viewer-0.1.0/docs/mcp.md +68 -0
  35. laranex_schema_viewer-0.1.0/docs/quick-start.md +53 -0
  36. laranex_schema_viewer-0.1.0/docs/sqlalchemy.md +31 -0
  37. laranex_schema_viewer-0.1.0/docs/sqlmodel.md +36 -0
  38. laranex_schema_viewer-0.1.0/docs/troubleshooting.md +32 -0
  39. laranex_schema_viewer-0.1.0/docs/validation.md +43 -0
  40. laranex_schema_viewer-0.1.0/docs/viewer.md +32 -0
  41. laranex_schema_viewer-0.1.0/package.json +10 -0
  42. laranex_schema_viewer-0.1.0/pyproject.toml +61 -0
  43. laranex_schema_viewer-0.1.0/scripts/browser-smoke.mjs +110 -0
  44. laranex_schema_viewer-0.1.0/scripts/check_wheel.py +45 -0
  45. laranex_schema_viewer-0.1.0/scripts/validate_reference.py +261 -0
  46. laranex_schema_viewer-0.1.0/skills/python-schema-viewer/SKILL.md +41 -0
  47. laranex_schema_viewer-0.1.0/skills/relationship-analysis/SKILL.md +34 -0
  48. laranex_schema_viewer-0.1.0/skills/schema-diagnostics/SKILL.md +32 -0
  49. laranex_schema_viewer-0.1.0/skills/schema-discovery/SKILL.md +40 -0
  50. laranex_schema_viewer-0.1.0/skills/schema-discovery/references/snapshot.md +28 -0
  51. laranex_schema_viewer-0.1.0/skills/schema-viewer-mcp/SKILL.md +65 -0
  52. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/__init__.py +8 -0
  53. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/__main__.py +4 -0
  54. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/adapters/__init__.py +10 -0
  55. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/adapters/sqlalchemy.py +229 -0
  56. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/build.py +100 -0
  57. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/cli.py +56 -0
  58. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/document.py +176 -0
  59. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/mcp.py +66 -0
  60. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/py.typed +0 -0
  61. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/query.py +117 -0
  62. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/schema.json +702 -0
  63. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/server.py +64 -0
  64. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/snapshot.py +50 -0
  65. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/static/app.js +289 -0
  66. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/static/graph.js +89 -0
  67. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/static/index.html +40 -0
  68. laranex_schema_viewer-0.1.0/src/laranex_schema_viewer/static/styles.css +523 -0
  69. laranex_schema_viewer-0.1.0/tests/__init__.py +0 -0
  70. laranex_schema_viewer-0.1.0/tests/dom.test.mjs +122 -0
  71. laranex_schema_viewer-0.1.0/tests/fixtures/__init__.py +0 -0
  72. laranex_schema_viewer-0.1.0/tests/fixtures/sqlalchemy_models.py +61 -0
  73. laranex_schema_viewer-0.1.0/tests/fixtures/sqlmodel_models.py +27 -0
  74. laranex_schema_viewer-0.1.0/tests/graph.test.mjs +61 -0
  75. laranex_schema_viewer-0.1.0/tests/test_distributed_skills.py +124 -0
  76. laranex_schema_viewer-0.1.0/tests/test_extraction.py +174 -0
  77. laranex_schema_viewer-0.1.0/tests/test_integration.py +146 -0
  78. laranex_schema_viewer-0.1.0/tests/test_reference_validation.py +22 -0
  79. laranex_schema_viewer-0.1.0/tests/test_snapshot_query.py +112 -0
@@ -0,0 +1,71 @@
1
+ import { defineConfig } from 'vitepress'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { createRequire } from 'node:module'
4
+
5
+ const base = '/python-schema-viewer/'
6
+ const repository = 'https://github.com/laranex/python-schema-viewer'
7
+ const require = createRequire(import.meta.url)
8
+
9
+ export default defineConfig({
10
+ title: 'Laranex Schema Viewer',
11
+ description: 'Explore SQLAlchemy and SQLModel definitions with an interactive ERD, portable JSON snapshots, and read-only MCP tools.',
12
+ lang: 'en-US',
13
+ base,
14
+ cleanUrls: false,
15
+ appearance: true,
16
+ srcDir: '../docs',
17
+ srcExclude: ['README.md', 'architecture.md', 'validation.md', 'agent-skills-validation.md'],
18
+ vite: {
19
+ publicDir: fileURLToPath(new URL('../public', import.meta.url)),
20
+ // Markdown lives outside the Node project; resolve Vue from this project.
21
+ resolve: { alias: [
22
+ { find: /^vue$/, replacement: require.resolve('vue/dist/vue.runtime.esm-bundler.js') },
23
+ { find: 'vue/server-renderer', replacement: require.resolve('vue/server-renderer') },
24
+ ] },
25
+ },
26
+ lastUpdated: false,
27
+ sitemap: { hostname: 'https://laranex.github.io/python-schema-viewer/' },
28
+ head: [
29
+ ['link', { rel: 'icon', type: 'image/svg+xml', href: `${base}logo.svg` }],
30
+ ['meta', { name: 'theme-color', content: '#0b7f6b' }],
31
+ ['meta', { property: 'og:type', content: 'website' }],
32
+ ['meta', { property: 'og:site_name', content: 'Laranex Schema Viewer' }],
33
+ ],
34
+ markdown: { theme: { light: 'github-light', dark: 'github-dark' }, lineNumbers: false },
35
+ themeConfig: {
36
+ logo: '/logo.svg',
37
+ siteTitle: 'Laranex',
38
+ nav: [
39
+ { text: 'Python Schema Viewer', link: '/introduction' },
40
+ { text: 'Live demo', link: '/demo' },
41
+ { text: 'Laranex projects ↗', link: 'https://laranex.vercel.app/' },
42
+ ],
43
+ sidebar: [
44
+ { text: 'GET STARTED', items: [
45
+ { text: 'Introduction', link: '/introduction' },
46
+ { text: 'Installation', link: '/installation' },
47
+ { text: 'Quick start', link: '/quick-start' },
48
+ ] },
49
+ { text: 'WORK WITH YOUR SCHEMA', items: [
50
+ { text: 'SQLAlchemy', link: '/sqlalchemy' },
51
+ { text: 'SQLModel', link: '/sqlmodel' },
52
+ { text: 'Discovery & extraction', link: '/discovery' },
53
+ { text: 'Interactive viewer', link: '/viewer' },
54
+ { text: 'Live demo', link: '/demo' },
55
+ ] },
56
+ { text: 'REFERENCE', items: [
57
+ { text: 'CLI & configuration', link: '/cli' },
58
+ { text: 'MCP for agents', link: '/mcp' },
59
+ { text: 'Agent skills', link: '/agent-skills' },
60
+ { text: 'Troubleshooting', link: '/troubleshooting' },
61
+ ] },
62
+ ],
63
+ search: { provider: 'local', options: { detailedView: true } },
64
+ outline: { level: [2, 3], label: 'On this page' },
65
+ socialLinks: [{ icon: 'github', link: repository }],
66
+ editLink: { pattern: `${repository}/edit/main/docs/:path`, text: 'Improve this page on GitHub' },
67
+ footer: { message: 'Open source under the MIT license.', copyright: 'Laranex · Built by developers, for developers.' },
68
+ docFooter: { prev: 'Previous page', next: 'Next page' },
69
+ externalLinkIcon: true,
70
+ },
71
+ })
@@ -0,0 +1,24 @@
1
+ <script setup>
2
+ import { computed, onMounted, ref, watch } from 'vue'
3
+ import { useData, withBase } from 'vitepress'
4
+
5
+ const { isDark } = useData()
6
+ const frame = ref(null)
7
+ const ready = ref(false)
8
+ const src = computed(() => withBase('/demo/index.html'))
9
+ function syncTheme() {
10
+ const root = frame.value?.contentDocument?.documentElement
11
+ if (root) root.dataset.theme = isDark.value ? 'dark' : 'light'
12
+ }
13
+ function loaded() { ready.value = true; syncTheme() }
14
+ watch(isDark, syncTheme)
15
+ onMounted(syncTheme)
16
+ </script>
17
+
18
+ <template>
19
+ <section class="lx-demo" aria-label="Synthetic schema demonstration">
20
+ <div class="lx-demo-bar"><span><i></i> INTERACTIVE DEMO</span><span>Synthetic workspace · no backend</span></div>
21
+ <iframe ref="frame" :src="src" title="Interactive ERD of a synthetic project workspace" loading="lazy" @load="loaded"></iframe>
22
+ <div class="lx-demo-footer"><span>{{ ready ? 'Drag to explore · scroll to zoom · select for details' : 'Loading the demo when it comes into view…' }}</span><a :href="src" target="_blank" rel="noopener">Open full screen ↗</a></div>
23
+ </section>
24
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup>
2
+ import { ref } from 'vue'
3
+ import { withBase } from 'vitepress'
4
+
5
+ const command = "pip install 'laranex-schema-viewer[sqlmodel,mcp]'"
6
+ const message = ref('Copy command')
7
+ async function copy() {
8
+ try {
9
+ await navigator.clipboard.writeText(command)
10
+ message.value = 'Copied'
11
+ } catch {
12
+ message.value = 'Select and copy the command below'
13
+ }
14
+ }
15
+ </script>
16
+
17
+ <template>
18
+ <header class="lx-package">
19
+ <p class="lx-eyebrow"><span></span> PYTHON TOOLING, WITH A CLEARER PICTURE</p>
20
+ <h1>Laranex<br /><span>Schema Viewer</span></h1>
21
+ <p class="lx-lead">Understand how your data fits together.<br />Tables, constraints, and ORM relationships — in one explorable view.</p>
22
+ <div class="lx-tags"><span>SQLAlchemy 2.x</span><span>SQLModel</span><span>Read-only MCP</span><span>MIT</span></div>
23
+ <div class="lx-install"><span aria-hidden="true">$</span><code>{{ command }}</code><button @click="copy" :aria-label="message">{{ message === 'Copied' ? 'Copied ✓' : 'Copy' }}</button></div>
24
+ <p class="lx-caption">Requires Python 3.10+. Choose the <code>sqlalchemy</code> or <code>sqlmodel</code> extra; add <code>mcp</code> for agents.</p>
25
+ <span class="sr-only" role="status">{{ message }}</span>
26
+ <div class="lx-actions"><a class="lx-button primary" :href="withBase('/quick-start.html')">Get started <span aria-hidden="true">→</span></a><a class="lx-button secondary" :href="withBase('/demo.html')">Explore the live demo</a></div>
27
+ </header>
28
+ </template>
@@ -0,0 +1,82 @@
1
+ /* Laranex design vocabulary, with VitePress's responsive navigation and search. */
2
+ :root {
3
+ --vp-font-family-base: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
4
+ --vp-font-family-mono: 'Geist Mono Variable', ui-monospace, monospace;
5
+ --vp-c-brand-1: #0b7f6b;
6
+ --vp-c-brand-2: #129e85;
7
+ --vp-c-brand-3: #18b69b;
8
+ --vp-c-brand-soft: rgba(24, 182, 155, .1);
9
+ --vp-c-text-1: #0e1a21;
10
+ --vp-c-text-2: #4d5d68;
11
+ --vp-c-bg-alt: #f5f8f9;
12
+ --vp-c-bg-soft: #f5f8f9;
13
+ --vp-c-divider: #e3e9ec;
14
+ --vp-code-block-bg: #f6f9fa;
15
+ --vp-layout-max-width: 1480px;
16
+ --vp-sidebar-width: 264px;
17
+ --vp-button-brand-bg: #0b7f6b;
18
+ --vp-button-brand-hover-bg: #096b5a;
19
+ }
20
+ .dark {
21
+ --vp-c-brand-1: #34d3b6;
22
+ --vp-c-brand-2: #2fcfb2;
23
+ --vp-c-brand-3: #18b69b;
24
+ --vp-c-text-1: #e6eff2;
25
+ --vp-c-text-2: #98abb4;
26
+ --vp-c-bg: #0a1014;
27
+ --vp-c-bg-alt: #080d10;
28
+ --vp-c-bg-soft: #10191f;
29
+ --vp-c-bg-elv: #121c22;
30
+ --vp-c-divider: #1e2b33;
31
+ --vp-code-block-bg: #0d151a;
32
+ --vp-c-brand-soft: rgba(24, 182, 155, .14);
33
+ }
34
+ body { -webkit-font-smoothing: antialiased; }
35
+ ::selection { background: rgba(24, 182, 155, .24); }
36
+ :focus-visible { outline: 2px solid var(--vp-c-brand-1); outline-offset: 4px; }
37
+ .VPNavBar { backdrop-filter: blur(16px); }
38
+ .VPNavBarTitle .title { font-weight: 650; letter-spacing: -.5px; }
39
+ .VPNavBarTitle .logo { width: 29px; height: 29px; }
40
+ .VPSidebarItem.level-0 > .item > .text { font-size: 10px; letter-spacing: 1.3px; font-weight: 650; color: var(--vp-c-text-2); }
41
+ .vp-doc h1 { letter-spacing: -1.5px; }
42
+ .vp-doc h2 { letter-spacing: -.65px; }
43
+ .vp-doc div[class*='language-'] { border: 1px solid var(--vp-c-divider); border-radius: 10px; }
44
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); }
45
+ .lx-package { padding: 12px 0 30px; position: relative; }
46
+ .lx-eyebrow { color: var(--vp-c-brand-1); font-size: 10px; letter-spacing: 1.8px; font-weight: 650; display: flex; align-items: center; gap: 9px; }
47
+ .lx-eyebrow span { width: 6px; height: 6px; border-radius: 50%; background: #18b69b; box-shadow: 0 0 0 4px var(--vp-c-brand-soft); }
48
+ .vp-doc .lx-package h1 { margin: 25px 0 20px; font-size: clamp(38px, 5.1vw, 66px); line-height: 1.07; letter-spacing: -2.5px; font-weight: 650; }
49
+ .lx-package h1 span { color: var(--vp-c-brand-1); }
50
+ .vp-doc .lx-lead { font-size: 18px; line-height: 1.75; color: var(--vp-c-text-2); }
51
+ .lx-tags { display: flex; gap: 8px; flex-wrap: wrap; margin: 22px 0; }
52
+ .lx-tags span { font-size: 11px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft); border-radius: 5px; padding: 2px 9px; }
53
+ .lx-install { display: flex; align-items: center; gap: 13px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft); border-radius: 9px; padding: 10px 14px; max-width: 640px; }
54
+ .lx-install > span { color: var(--vp-c-brand-1); }
55
+ .vp-doc .lx-install code { color: var(--vp-c-text-1); background: transparent; font-size: 12px; overflow-x: auto; padding: 0; white-space: nowrap; }
56
+ .lx-install button { font-size: 10px; padding: 3px 8px; border: 1px solid var(--vp-c-divider); border-radius: 5px; margin-left: auto; }
57
+ .vp-doc .lx-caption { font-size: 11px; color: var(--vp-c-text-2); margin-top: 7px; }
58
+ .lx-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 24px; }
59
+ .vp-doc .lx-button { border-radius: 7px; font-size: 13px; padding: 8px 17px; text-decoration: none; font-weight: 550; }
60
+ .lx-button.primary { background: #0b7f6b; color: white; }
61
+ .lx-button.primary:hover { background: #096b5a; }
62
+ .lx-button.secondary { border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); }
63
+ .lx-feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; margin: 15px 0 30px; }
64
+ .lx-feature { padding: 20px 0; border-bottom: 1px solid var(--vp-c-divider); }
65
+ .lx-feature strong { font-size: 14px; }
66
+ .vp-doc .lx-feature p { color: var(--vp-c-text-2); font-size: 13px; line-height: 1.75; margin: 8px 0 0; }
67
+ .lx-demo { border: 1px solid var(--vp-c-divider); border-radius: 12px; overflow: hidden; margin: 28px 0 20px; background: var(--vp-c-bg-soft); box-shadow: 0 12px 40px -20px rgba(0, 0, 0, .2); }
68
+ .lx-demo-bar, .lx-demo-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; font-size: 10px; color: var(--vp-c-text-2); }
69
+ .lx-demo-bar { border-bottom: 1px solid var(--vp-c-divider); }
70
+ .lx-demo-bar span:first-child { letter-spacing: 1px; font-weight: 600; }
71
+ .lx-demo-bar i { display: inline-block; width: 5px; height: 5px; background: #18b69b; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
72
+ .lx-demo iframe { display: block; width: 100%; height: 600px; border: 0; background: var(--vp-c-bg); }
73
+ .lx-demo-footer { border-top: 1px solid var(--vp-c-divider); }
74
+ .lx-demo-footer a { white-space: nowrap; }
75
+ @media (min-width: 1280px) { .VPDoc.has-aside .content-container { max-width: 760px; } }
76
+ @media (max-width: 640px) {
77
+ .lx-feature-grid { grid-template-columns: 1fr; }
78
+ .vp-doc .lx-lead { font-size: 16px; }
79
+ .lx-demo iframe { height: 700px; }
80
+ .lx-demo-bar span:last-child, .lx-demo-footer span { display: none; }
81
+ .lx-demo-footer { justify-content: flex-end; }
82
+ }
@@ -0,0 +1,14 @@
1
+ import '@fontsource-variable/geist'
2
+ import '@fontsource-variable/geist-mono'
3
+ import DefaultTheme from 'vitepress/theme'
4
+ import PackageIntroduction from './PackageIntroduction.vue'
5
+ import LiveDemo from './LiveDemo.vue'
6
+ import './custom.css'
7
+
8
+ export default {
9
+ extends: DefaultTheme,
10
+ enhanceApp({ app }) {
11
+ app.component('PackageIntroduction', PackageIntroduction)
12
+ app.component('LiveDemo', LiveDemo)
13
+ },
14
+ }
@@ -0,0 +1,76 @@
1
+ // Entirely synthetic project workspace. Never import application/reference metadata.
2
+ const tid = name => `demo:${name}`;
3
+ const mid = name => `model:${name}`;
4
+ const col = (name, type = 'INTEGER', options = {}) => ({
5
+ name, type, timezone: null, nullable: false, primary_key: name === 'id',
6
+ autoincrement: 'auto', default: null, server_default: null, comment: null, ...options,
7
+ });
8
+ const fk = (table, column, target) => ({
9
+ id: `fk:${table}:${column}`, name: `fk_${table}_${column}`, columns: [column],
10
+ target_table: tid(target), target_columns: ['id'], on_delete: 'RESTRICT',
11
+ on_update: null, deferrable: null, initially: null,
12
+ });
13
+ const index = (name, columns, unique = false, options = {}) => ({
14
+ name, columns, expressions: [...columns], unique, predicates: {}, included_columns: {}, methods: {}, ...options,
15
+ });
16
+ const table = (name, columns, foreign_keys = [], indexes = []) => ({
17
+ id: tid(name), namespace: 'workspace', name, schema_name: null,
18
+ comment: 'Synthetic public documentation example', columns: [col('id'), ...columns],
19
+ primary_key: ['id'], indexes, foreign_keys, constraints: [],
20
+ });
21
+ const pair = (localTable, localColumn, remoteTable, remoteColumn) => ({
22
+ local: {table: tid(localTable), column: localColumn},
23
+ remote: {table: tid(remoteTable), column: remoteColumn},
24
+ });
25
+ function relation(source, name, target, direction, join_pairs, secondary = null) {
26
+ return {
27
+ id: `orm:${source}:${name}`, name, model: mid(source), target_model: mid(target),
28
+ source_table: tid(source), target_table: tid(target), direction,
29
+ uselist: direction !== 'MANYTOONE', viewonly: false, back_populates: null,
30
+ secondary: secondary ? tid(secondary) : null,
31
+ primary_join: join_pairs.map(p => `${p.local.table}.${p.local.column} = ${p.remote.table}.${p.remote.column}`).join(' AND '),
32
+ secondary_join: secondary ? `${secondary}.${target.slice(0, -1)}_id = ${target}.id` : null,
33
+ join_pairs,
34
+ };
35
+ }
36
+
37
+ export const schema = {
38
+ format: 'laranex-schema-viewer', version: 1, scope: 'declared_metadata',
39
+ tables: [
40
+ table('users', [col('email', 'VARCHAR(254)'), col('manager_id', 'INTEGER', {nullable: true}), col('active', 'BOOLEAN')],
41
+ [fk('users', 'manager_id', 'users')],
42
+ [index('uq_users_active_email', ['email'], true, {predicates: {postgresql: 'active = true'}})]),
43
+ table('teams', [col('name', 'VARCHAR(100)')]),
44
+ table('team_members', [col('user_id'), col('team_id')],
45
+ [fk('team_members', 'user_id', 'users'), fk('team_members', 'team_id', 'teams')],
46
+ [index('uq_team_members_pair', ['team_id', 'user_id'], true)]),
47
+ table('projects', [col('name', 'VARCHAR(120)'), col('owner_id'), col('team_id')],
48
+ [fk('projects', 'owner_id', 'users'), fk('projects', 'team_id', 'teams')]),
49
+ table('tasks', [col('title', 'VARCHAR(200)'), col('project_id'), col('assignee_id'), col('parent_id', 'INTEGER', {nullable: true}), col('metadata', 'JSONB')],
50
+ [fk('tasks', 'project_id', 'projects'), fk('tasks', 'assignee_id', 'users'), fk('tasks', 'parent_id', 'tasks')],
51
+ [index('ix_tasks_project', ['project_id']), index('ix_tasks_lower_title', [], false, {expressions: ['lower(title)']})]),
52
+ table('comments', [col('body', 'TEXT'), col('task_id'), col('user_id')],
53
+ [fk('comments', 'task_id', 'tasks'), fk('comments', 'user_id', 'users')]),
54
+ table('tags', [col('name', 'VARCHAR(40)')]),
55
+ table('task_tags', [col('task_id'), col('tag_id')],
56
+ [fk('task_tags', 'task_id', 'tasks'), fk('task_tags', 'tag_id', 'tags')]),
57
+ table('audit_events', [col('actor_id'), col('event', 'VARCHAR(80)'), col('created_at', 'DATETIME', {timezone: true})],
58
+ [fk('audit_events', 'actor_id', 'users')]),
59
+ ],
60
+ models: ['users', 'teams', 'projects', 'tasks', 'comments', 'tags'].map(name => ({
61
+ id: mid(name), class_name: `demo.models.${name.split('_').map(s => s[0].toUpperCase() + s.slice(1)).join('')}Model`,
62
+ table_ids: [tid(name)], primary_key: ['id'], source: null,
63
+ })),
64
+ relationships: [
65
+ relation('users', 'manager', 'users', 'MANYTOONE', [pair('users', 'manager_id', 'users', 'id')]),
66
+ relation('users', 'teams', 'teams', 'MANYTOMANY', [pair('users', 'id', 'team_members', 'user_id'), pair('teams', 'id', 'team_members', 'team_id')], 'team_members'),
67
+ relation('teams', 'members', 'users', 'MANYTOMANY', [pair('teams', 'id', 'team_members', 'team_id'), pair('users', 'id', 'team_members', 'user_id')], 'team_members'),
68
+ relation('projects', 'tasks', 'tasks', 'ONETOMANY', [pair('projects', 'id', 'tasks', 'project_id')]),
69
+ relation('tasks', 'project', 'projects', 'MANYTOONE', [pair('tasks', 'project_id', 'projects', 'id')]),
70
+ relation('tasks', 'assignee', 'users', 'MANYTOONE', [pair('tasks', 'assignee_id', 'users', 'id')]),
71
+ relation('tasks', 'comments', 'comments', 'ONETOMANY', [pair('tasks', 'id', 'comments', 'task_id')]),
72
+ relation('comments', 'task', 'tasks', 'MANYTOONE', [pair('comments', 'task_id', 'tasks', 'id')]),
73
+ relation('tasks', 'tags', 'tags', 'MANYTOMANY', [pair('tasks', 'id', 'task_tags', 'task_id'), pair('tags', 'id', 'task_tags', 'tag_id')], 'task_tags'),
74
+ ],
75
+ diagnostics: [],
76
+ };