diff --git a/AGENTS.md b/AGENTS.md index 938a2b02..c71b42b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ A pnpm monorepo for PostgreSQL AST parsing, deparsing, and code generation. All | `@pgsql/utils` | `packages/utils` | Type-safe AST node creation utilities | | `@pgsql/traverse` | `packages/traverse` | Visitor-pattern AST traversal | | `@pgsql/transform-ast` | `packages/transform-ast` | Multi-version AST transformer (PG 13-17) | -| `@pgsql/transform` | `packages/transform` | SQL schema transformation, statement classification, qualification, dependency closure | +| `@pgsql/transform` | `packages/transform` | SQL schema transformation, statement classification (AST facts), qualification, round-trip validation | | `@pgsql/quotes` | `packages/quotes` | SQL identifier/string quoting and keyword classification | | `@pgsql/cli` | `packages/pgsql-cli` | CLI tool for parse/deparse operations | | `pg-proto-parser` | `packages/proto-parser` | Generate TypeScript from PostgreSQL protobuf definitions | diff --git a/README.md b/README.md index 64330fb1..21603b84 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ walk(ast, visitor); | [**@pgsql/utils**](./packages/utils) | Type-safe AST node creation utilities | • Programmatic AST construction
• Runtime Schema
• Seamless integration with types | | [**pg-proto-parser**](./packages/proto-parser) | PostgreSQL protobuf parser and code generator | • Generate TypeScript interfaces from protobuf
• Create enum mappings and utilities
• AST helper generation | | [**@pgsql/transform-ast**](./packages/transform-ast) | Multi-version PostgreSQL AST transformer | • Transform ASTs between PostgreSQL versions (13→17)
• Single source of truth deparser pipeline
• Backward compatibility for legacy SQL | -| [**@pgsql/transform**](./packages/transform) | SQL transformation & classification | • Schema-name rewriting (incl. PL/pgSQL bodies)
• Per-statement AST facts (`classifyStatements`)
• Transitive dependency closure & qualification | +| [**@pgsql/transform**](./packages/transform) | SQL transformation & classification | • Schema-name rewriting (incl. PL/pgSQL bodies)
• Per-statement AST facts (`classifyStatements`)
• Qualification & round-trip validation | | [**@pgsql/traverse**](./packages/traverse) | PostgreSQL AST traversal utilities | • Visitor pattern for traversing PostgreSQL AST nodes
• NodePath context with parent/path information
• Runtime schema-based precise traversal | ## 🛠️ Development diff --git a/packages/transform/README.md b/packages/transform/README.md index 67f270da..50deb332 100644 --- a/packages/transform/README.md +++ b/packages/transform/README.md @@ -54,14 +54,6 @@ const facts = classifyStatements(sql); Qualify unqualified object references against an inventory of known objects, with multi-schema routing support. -### `resolveFixtureClosure` — transitive dependency closure - -Given a set of changes (name + SQL + optional declared dependencies), compute the transitive closure of a selection: forward producers of every referenced object/schema/role, plus attached fixtures (policies/grants/RLS targeting closure members), with explicit unresolved-reference reporting. - -### `categorizeChange` / `buildCategoryOf` — change categorization - -Profile-driven categorization of migration changes (e.g. schema / functionality / security / fixtures) from their AST facts. - ### Round-trip validation `normalizeTree` / `cleanTree` / `validateRoundTrip` — dependency-free AST normalization and mutation-aware parse→deparse→re-parse validation. diff --git a/packages/transform/__tests__/categorize.test.ts b/packages/transform/__tests__/categorize.test.ts deleted file mode 100644 index e7dbeb35..00000000 --- a/packages/transform/__tests__/categorize.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { loadModule } from 'plpgsql-parser'; - -import { buildCategoryOf, categorizeChange, CategoryProfile, TIER_PROFILE } from '../src/categorize'; - -beforeAll(async () => { - await loadModule(); -}); - -describe('categorizeChange (tier profile)', () => { - it('classifies base DDL as schema', () => { - expect(categorizeChange('CREATE SCHEMA app_public;', 'schemas/app/schema')).toBe('schema'); - expect( - categorizeChange('CREATE TABLE app_public.users (id uuid PRIMARY KEY);', 'schemas/app/tables/users') - ).toBe('schema'); - expect( - categorizeChange('CREATE INDEX users_org_idx ON app_public.users (org_id);', 'schemas/app/indexes/users_org') - ).toBe('schema'); - }); - - it('classifies functions and triggers as functionality', () => { - expect( - categorizeChange( - 'CREATE FUNCTION app_public.touch() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;', - 'schemas/app/procedures/touch' - ) - ).toBe('functionality'); - expect( - categorizeChange( - 'CREATE TRIGGER touch_users BEFORE UPDATE ON app_public.users FOR EACH ROW EXECUTE FUNCTION app_public.touch();', - 'schemas/app/triggers/touch_users' - ) - ).toBe('functionality'); - }); - - it('classifies policies, grants and RLS as security (wins over everything)', () => { - expect( - categorizeChange('GRANT SELECT ON app_public.users TO authenticated;', 'schemas/app/grants/users') - ).toBe('security'); - expect( - categorizeChange( - 'CREATE POLICY users_select ON app_public.users FOR SELECT TO authenticated USING (true);', - 'schemas/app/policies/users_select' - ) - ).toBe('security'); - // A change that both creates a table AND enables RLS is pulled into security. - expect( - categorizeChange( - 'CREATE TABLE app_public.secrets (id uuid PRIMARY KEY); ALTER TABLE app_public.secrets ENABLE ROW LEVEL SECURITY;', - 'schemas/app/tables/secrets' - ) - ).toBe('security'); - }); - - it('classifies seed DML as fixtures', () => { - expect( - categorizeChange("INSERT INTO app_public.settings (key, value) VALUES ('theme', 'dark');", 'seeds/settings') - ).toBe('fixtures'); - }); - - it('defaults empty/unclassifiable SQL to schema', () => { - expect(categorizeChange('-- just a comment\n', 'schemas/app/noop')).toBe('schema'); - }); -}); - -describe('buildCategoryOf', () => { - const changeSql: Record = { - 'schemas/app/schema': 'CREATE SCHEMA app_public;', - 'schemas/app/tables/users': 'CREATE TABLE app_public.users (id uuid PRIMARY KEY);', - 'schemas/app/procedures/touch': - 'CREATE FUNCTION app_public.touch() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;', - 'schemas/app/grants/users': 'GRANT SELECT ON app_public.users TO authenticated;', - }; - - it('produces a categoryOf function keyed by change name', () => { - const categoryOf = buildCategoryOf(changeSql); - expect(categoryOf('schemas/app/schema')).toBe('schema'); - expect(categoryOf('schemas/app/tables/users')).toBe('schema'); - expect(categoryOf('schemas/app/procedures/touch')).toBe('functionality'); - expect(categoryOf('schemas/app/grants/users')).toBe('security'); - }); - - it('returns undefined for changes it never saw (core falls back to folder key)', () => { - const categoryOf = buildCategoryOf(changeSql); - expect(categoryOf('schemas/other/unknown')).toBeUndefined(); - }); - - it('honors a custom profile', () => { - // A per-schema profile: the category is the top-level schema segment. - const bySchema: CategoryProfile = { - name: 'by-schema', - categorize: (_facts, changeName) => changeName.split('/')[1] ?? 'root', - }; - const categoryOf = buildCategoryOf(changeSql, bySchema); - expect(categoryOf('schemas/app/schema')).toBe('app'); - }); - - it('exposes the default profile name', () => { - expect(TIER_PROFILE.name).toBe('tier'); - }); -}); diff --git a/packages/transform/__tests__/fixture-closure.test.ts b/packages/transform/__tests__/fixture-closure.test.ts deleted file mode 100644 index 3a344f26..00000000 --- a/packages/transform/__tests__/fixture-closure.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { loadModule } from 'plpgsql-parser'; - -import { ClosureInputChange, resolveFixtureClosure } from '../src/fixture-closure'; - -beforeAll(async () => { - await loadModule(); -}); - -// A small module: two schemas, tables, a policy + grant + RLS (security), -// a seed insert (fixtures), and a trigger function (functionality). -const CHANGES: ClosureInputChange[] = [ - { name: 'schemas/app_public/schema', sql: 'CREATE SCHEMA app_public;' }, - { name: 'schemas/app_private/schema', sql: 'CREATE SCHEMA app_private;' }, - { - name: 'schemas/app_public/tables/orgs', - sql: 'CREATE TABLE app_public.orgs (id uuid PRIMARY KEY);', - dependencies: ['schemas/app_public/schema'] - }, - { - name: 'schemas/app_public/tables/users', - sql: 'CREATE TABLE app_public.users (id uuid PRIMARY KEY, org_id uuid REFERENCES app_public.orgs (id));', - dependencies: ['schemas/app_public/schema'] - }, - { - name: 'schemas/app_private/tables/sprt_org_members', - sql: 'CREATE TABLE app_private.sprt_org_members (user_id uuid, org_id uuid REFERENCES app_public.orgs (id));', - dependencies: ['schemas/app_private/schema'] - }, - { - name: 'schemas/app_public/policies/users_select', - sql: `CREATE POLICY users_select ON app_public.users FOR SELECT TO authenticated - USING (org_id IN (SELECT org_id FROM app_private.sprt_org_members));` - }, - { - name: 'schemas/app_public/grants/users_grant', - sql: 'GRANT SELECT ON app_public.users TO authenticated;' - }, - { - name: 'schemas/app_public/rls/users_enable', - sql: 'ALTER TABLE app_public.users ENABLE ROW LEVEL SECURITY;' - }, - { - name: 'schemas/app_public/seed/default_org', - sql: "INSERT INTO app_public.orgs (id) VALUES ('00000000-0000-0000-0000-000000000000');" - }, - // an entirely unrelated table (same schema) that must NOT be pulled in - { - name: 'schemas/other/tables/widgets', - sql: 'CREATE TABLE app_public.widgets (id uuid PRIMARY KEY);', - dependencies: ['schemas/app_public/schema'] - }, - // a grant on the unrelated table — shares the schema but must NOT attach to users - { - name: 'schemas/other/grants/widgets_grant', - sql: 'GRANT SELECT ON app_public.widgets TO authenticated;' - } -]; - -describe('resolveFixtureClosure', () => { - it('pulls prerequisites and attached fixtures for a selected table', () => { - const closure = resolveFixtureClosure(CHANGES, ['schemas/app_public/tables/users']); - - // prerequisites: schema + FK target table - expect(closure.order).toContain('schemas/app_public/schema'); - expect(closure.order).toContain('schemas/app_public/tables/orgs'); - - // attached fixtures: policy, grant, RLS enable, and the seed on orgs - expect(closure.fixtures).toEqual( - expect.arrayContaining([ - 'schemas/app_public/policies/users_select', - 'schemas/app_public/grants/users_grant', - 'schemas/app_public/rls/users_enable', - 'schemas/app_public/seed/default_org' - ]) - ); - - // the policy references the SPRT table -> its prerequisite is pulled too - expect(closure.order).toContain('schemas/app_private/tables/sprt_org_members'); - expect(closure.order).toContain('schemas/app_private/schema'); - - // unrelated table and its grant stay out despite sharing the schema - expect(closure.order).not.toContain('schemas/other/tables/widgets'); - expect(closure.order).not.toContain('schemas/other/grants/widgets_grant'); - }); - - it('is deterministic and preserves plan order', () => { - const a = resolveFixtureClosure(CHANGES, ['schemas/app_public/tables/users']); - const b = resolveFixtureClosure(CHANGES, ['schemas/app_public/tables/users']); - expect(a).toEqual(b); - - const planOrder = CHANGES.map(c => c.name); - const positions = a.order.map(n => planOrder.indexOf(n)); - expect(positions).toEqual([...positions].sort((x, y) => x - y)); - }); - - it('reports required roles and surfaces unresolved external roles', () => { - const closure = resolveFixtureClosure(CHANGES, ['schemas/app_public/tables/users']); - // `authenticated` is referenced by policy/grant but created by no change - expect(closure.roles).toContain('authenticated'); - expect(closure.unresolved.roles).toContain('authenticated'); - }); - - it('labels each closure member with a category and reason', () => { - const closure = resolveFixtureClosure(CHANGES, ['schemas/app_public/tables/users']); - const users = closure.changes.find(c => c.name === 'schemas/app_public/tables/users')!; - const policy = closure.changes.find(c => c.name === 'schemas/app_public/policies/users_select')!; - const orgs = closure.changes.find(c => c.name === 'schemas/app_public/tables/orgs')!; - - expect(users.reason).toBe('selected'); - expect(users.category).toBe('schema'); - expect(policy.reason).toBe('fixture'); - expect(policy.category).toBe('security'); - expect(orgs.reason).toBe('prerequisite'); - }); - - it('can skip prerequisites while still pulling attached fixtures', () => { - const closure = resolveFixtureClosure(CHANGES, ['schemas/app_public/tables/users'], { - includePrerequisites: false - }); - // still pulls the fixtures attached to users - expect(closure.fixtures).toEqual( - expect.arrayContaining(['schemas/app_public/grants/users_grant']) - ); - // but does not walk references to the FK target as a prerequisite - expect(closure.order).not.toContain('schemas/app_public/tables/orgs'); - }); -}); diff --git a/packages/transform/src/categorize.ts b/packages/transform/src/categorize.ts deleted file mode 100644 index e2ddc810..00000000 --- a/packages/transform/src/categorize.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { classifyStatements, StatementFacts } from './facts'; - -/** - * A category is a chunk-seam key a change is assigned to. Free-form string; the - * built-in {@link TIER_PROFILE} uses `'security' | 'functionality' | 'fixtures' - * | 'schema'`. - * - * This module is the classifier-driven counterpart to `@pgpmjs/core`'s - * `boundary: 'category'` rebundle mode: core stays classifier-agnostic and only - * consumes a `categoryOf(changeName)` seam function, while the rules that decide - * *what* each change is (the profile) live here, next to the AST facts. - */ -export type ChangeCategory = string; - -/** - * Decides a change's category from the AST facts of its (deploy) statements. - * A profile is a pure function of the facts — swap it to carve a monolith along - * different seams (e.g. per-role admin/users modules) without touching core. - */ -export interface CategoryProfile { - /** Identifier for the profile (for logging / provenance). */ - name: string; - /** Map a change's statement facts to its category. */ - categorize: (facts: StatementFacts[], changeName: string) => ChangeCategory; -} - -/** - * Default tier profile: isolates the security surface and procedural code out - * of the schema base. Precedence, highest first — a change is placed in the - * most specialized tier any of its statements reaches: - * - * security any securityRelevant statement (policy/grant/RLS/role/owner) - * functionality functions and triggers - * fixtures seed DML (INSERT/UPDATE/DELETE) - * schema schemas, tables, views, indexes, types, constraints (base) - * - * Ordering/dependencies across the resulting chunks are still resolved by - * core's rebundle graph, so this only decides *naming/grouping*, never deploy - * order. - */ -export const TIER_PROFILE: CategoryProfile = { - name: 'tier', - categorize(facts): ChangeCategory { - if (facts.length === 0) return 'schema'; - if (facts.some(f => f.securityRelevant)) return 'security'; - if (facts.some(f => f.kind === 'function' || f.kind === 'trigger')) return 'functionality'; - if (facts.some(f => f.kind === 'seed_dml')) return 'fixtures'; - return 'schema'; - }, -}; - -/** - * Classify a single change's deploy SQL into its category under `profile` - * (defaults to {@link TIER_PROFILE}). - */ -export function categorizeChange( - sql: string, - changeName: string, - profile: CategoryProfile = TIER_PROFILE -): ChangeCategory { - return profile.categorize(classifyStatements(sql), changeName); -} - -/** - * Build a `categoryOf(changeName)` seam function from a map of change name to - * its deploy SQL — ready to pass straight to `@pgpmjs/core`'s rebundle - * `boundary: 'category'` / `categoryOf` options. - * - * Changes absent from `changeSql` return `undefined`, letting core fall back to - * its folder key for anything the classifier did not see. - */ -export function buildCategoryOf( - changeSql: Record, - profile: CategoryProfile = TIER_PROFILE -): (changeName: string) => ChangeCategory | undefined { - const categories = new Map(); - for (const [name, sql] of Object.entries(changeSql)) { - categories.set(name, categorizeChange(sql, name, profile)); - } - return (changeName: string): ChangeCategory | undefined => categories.get(changeName); -} diff --git a/packages/transform/src/fixture-closure.ts b/packages/transform/src/fixture-closure.ts deleted file mode 100644 index d3679544..00000000 --- a/packages/transform/src/fixture-closure.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { - CategoryProfile, - ChangeCategory, - TIER_PROFILE -} from './categorize'; -import { classifyStatements, QualifiedName, StatementFacts } from './facts'; - -/** - * A change plus its deploy SQL. Optional plan `dependencies` (from `pgpm.plan`) - * are followed as authoritative prerequisites in addition to AST-derived edges. - */ -export interface ClosureInputChange { - name: string; - sql: string; - dependencies?: string[]; -} - -/** Why a change ended up in the closure. */ -export type ClosureReason = 'selected' | 'prerequisite' | 'fixture'; - -/** - * A change included in the closure, with its category and the reason it was pulled. - */ -export interface ClosureChange { - name: string; - category: ChangeCategory; - reason: ClosureReason; -} - -/** - * The fixture closure of a schema slice. - * - * Everything the selected changes need to actually *work*: schema/functionality - * prerequisites they depend on, plus the fixtures that attach to them (policies, - * grants, RLS enables, role bindings, seed rows / SPRT-style support changes). - * References that nothing in the input produces are surfaced in `unresolved` - * rather than silently dropped. - */ -export interface FixtureClosure { - /** Full closure (selection + prerequisites + fixtures) in input/plan order. */ - order: string[]; - /** Per-change detail in the same order as {@link order}. */ - changes: ClosureChange[]; - /** Closure members categorized as security/fixtures (attached fixtures). */ - fixtures: string[]; - /** Non-selected schema/functionality members pulled in as prerequisites. */ - prerequisites: string[]; - /** Roles required across the closure. */ - roles: string[]; - /** Requirements no input change produces — explicit, never dropped. */ - unresolved: { - objects: QualifiedName[]; - schemas: string[]; - roles: string[]; - }; -} - -export interface ResolveFixtureClosureOptions { - /** Category profile used to label changes (default {@link TIER_PROFILE}). */ - profile?: CategoryProfile; - /** - * When false, do not follow AST references from selected changes to their - * schema/functionality producers (only pull attached fixtures + plan deps). - * Default true. - */ - includePrerequisites?: boolean; -} - -function objectKey(schema: string | null, name: string): string { - return `${schema ?? ''}\u0000${name}`; -} - -interface ChangeAnalysis { - name: string; - category: ChangeCategory; - facts: StatementFacts[]; - /** Declared plan dependencies (authoritative prerequisites). */ - dependencies: string[]; - /** Objects this change creates (for producer indexing). */ - creates: QualifiedName[]; - /** Schemas this change creates. */ - createsSchemas: string[]; - /** Roles this change creates (CreateRoleStmt). */ - createsRoles: string[]; - /** Schema-qualified objects this change requires. */ - needsObjects: QualifiedName[]; - /** Schemas this change requires. */ - needsSchemas: string[]; - /** Roles this change requires (excludes roles it creates itself). */ - needsRoles: string[]; - /** True when categorized as an attached fixture (security/fixtures). */ - isFixture: boolean; -} - -function analyze(change: ClosureInputChange, profile: CategoryProfile): ChangeAnalysis { - const facts = classifyStatements(change.sql); - const category = profile.categorize(facts, change.name); - - const creates: QualifiedName[] = []; - const createsSchemas: string[] = []; - const createsRoles: string[] = []; - const needsObjects: QualifiedName[] = []; - const needsSchemas = new Set(); - const needsRoles = new Set(); - - for (const f of facts) { - for (const c of f.creates) { - if (f.kind === 'schema') createsSchemas.push(c.name); - else { - creates.push(c); - if (c.schema) needsSchemas.add(c.schema); - } - } - if (f.nodeTag === 'CreateRoleStmt') createsRoles.push(...f.roles); - - for (const r of [...f.references, ...f.fkTargets]) { - if (r.schema) needsObjects.push(r); - } - for (const s of f.referencedSchemas) needsSchemas.add(s); - } - - const created = new Set(createsRoles); - for (const f of facts) { - if (f.nodeTag === 'CreateRoleStmt') continue; - for (const role of f.roles) if (!created.has(role)) needsRoles.add(role); - } - for (const s of createsSchemas) needsSchemas.delete(s); - - return { - name: change.name, - category, - facts, - dependencies: change.dependencies ?? [], - creates, - createsSchemas, - createsRoles, - needsObjects, - needsSchemas: [...needsSchemas], - needsRoles: [...needsRoles], - isFixture: category === 'security' || category === 'fixtures' - }; -} - -/** - * Resolve the fixture closure of a schema slice. - * - * Pure and deterministic — no I/O — and runs to a fixpoint in both directions: - * - forward: a selected change pulls in the producers of the objects/schemas/ - * roles it references, plus its declared plan dependencies (prerequisites); - * - reverse: any fixture (security/fixtures-tier change) that attaches to an - * object already in the closure is pulled in (attached fixtures), and then - * resolved for its own prerequisites. - * - * Output order follows the input (plan) order, so it stays deploy-safe. - */ -export function resolveFixtureClosure( - allChanges: ClosureInputChange[], - selection: string[], - options: ResolveFixtureClosureOptions = {} -): FixtureClosure { - const profile = options.profile ?? TIER_PROFILE; - const includePrerequisites = options.includePrerequisites ?? true; - - const analyses = allChanges.map(c => analyze(c, profile)); - const byName = new Map(analyses.map(a => [a.name, a])); - - const producerByObject = new Map(); - const producerBySchema = new Map(); - const producerByRole = new Map(); - for (const a of analyses) { - for (const c of a.creates) { - const key = objectKey(c.schema, c.name); - if (!producerByObject.has(key)) producerByObject.set(key, a.name); - } - for (const s of a.createsSchemas) { - if (!producerBySchema.has(s)) producerBySchema.set(s, a.name); - } - for (const r of a.createsRoles) { - if (!producerByRole.has(r)) producerByRole.set(r, a.name); - } - } - - const reason = new Map(); - for (const name of selection) { - if (byName.has(name)) reason.set(name, 'selected'); - } - - const producedObjectKeys = new Set(); - const producedSchemas = new Set(); - const refreshProduced = (): void => { - producedObjectKeys.clear(); - producedSchemas.clear(); - for (const name of reason.keys()) { - const a = byName.get(name)!; - for (const c of a.creates) producedObjectKeys.add(objectKey(c.schema, c.name)); - for (const s of a.createsSchemas) producedSchemas.add(s); - } - }; - - let changed = true; - while (changed) { - changed = false; - - // forward: pull producers of what current members need - for (const name of [...reason.keys()]) { - const a = byName.get(name)!; - const pull = (producer: string | undefined, r: ClosureReason): void => { - if (!producer || reason.has(producer)) return; - const pa = byName.get(producer)!; - reason.set(producer, pa.isFixture ? 'fixture' : r); - changed = true; - }; - for (const dep of a.dependencies) { - pull(byName.has(dep) ? dep : undefined, 'prerequisite'); - } - if (!includePrerequisites) continue; - for (const o of a.needsObjects) pull(producerByObject.get(objectKey(o.schema, o.name)), 'prerequisite'); - for (const s of a.needsSchemas) pull(producerBySchema.get(s), 'prerequisite'); - for (const role of a.needsRoles) pull(producerByRole.get(role), 'prerequisite'); - } - - // reverse: pull attached fixtures that target current members - refreshProduced(); - for (const a of analyses) { - if (reason.has(a.name) || !a.isFixture) continue; - const objectAttaches = - a.needsObjects.some(o => producedObjectKeys.has(objectKey(o.schema, o.name))) || - a.creates.some(c => producedObjectKeys.has(objectKey(c.schema, c.name))); - // Schema-scoped fixtures (e.g. ALTER DEFAULT PRIVILEGES IN SCHEMA x) name no - // specific object; attach them by schema. Object-scoped fixtures attach only - // via their object refs, so a shared schema does not drag in every fixture. - const schemaScoped = a.needsObjects.length === 0 && a.creates.length === 0; - const schemaAttaches = - schemaScoped && - (a.needsSchemas.some(s => producedSchemas.has(s)) || - a.createsSchemas.some(s => producedSchemas.has(s))); - if (objectAttaches || schemaAttaches) { - reason.set(a.name, 'fixture'); - changed = true; - } - } - } - - const order = analyses.filter(a => reason.has(a.name)).map(a => a.name); - const changes: ClosureChange[] = order.map(name => { - const a = byName.get(name)!; - return { name, category: a.category, reason: reason.get(name)! }; - }); - - const roles = new Set(); - const unresolvedObjects: QualifiedName[] = []; - const unresolvedObjectKeys = new Set(); - const unresolvedSchemas = new Set(); - const unresolvedRoles = new Set(); - - for (const name of order) { - const a = byName.get(name)!; - for (const role of a.needsRoles) { - roles.add(role); - if (!producerByRole.has(role)) unresolvedRoles.add(role); - } - for (const o of a.needsObjects) { - const key = objectKey(o.schema, o.name); - if (!producerByObject.has(key) && !unresolvedObjectKeys.has(key)) { - unresolvedObjectKeys.add(key); - unresolvedObjects.push(o); - } - } - for (const s of a.needsSchemas) { - if (!producerBySchema.has(s)) unresolvedSchemas.add(s); - } - } - - return { - order, - changes, - fixtures: changes.filter(c => c.reason === 'fixture').map(c => c.name), - prerequisites: changes.filter(c => c.reason === 'prerequisite').map(c => c.name), - roles: [...roles], - unresolved: { - objects: unresolvedObjects, - schemas: [...unresolvedSchemas], - roles: [...unresolvedRoles] - } - }; -} diff --git a/packages/transform/src/index.ts b/packages/transform/src/index.ts index cd447917..81b58f38 100644 --- a/packages/transform/src/index.ts +++ b/packages/transform/src/index.ts @@ -1,26 +1,9 @@ -export type { - CategoryProfile, - ChangeCategory, -} from './categorize'; -export { - buildCategoryOf, - categorizeChange, - TIER_PROFILE, -} from './categorize'; export type { QualifiedName, StatementFacts, StatementKind, } from './facts'; export { classifyStatements } from './facts'; -export type { - ClosureChange, - ClosureInputChange, - ClosureReason, - FixtureClosure, - ResolveFixtureClosureOptions, -} from './fixture-closure'; -export { resolveFixtureClosure } from './fixture-closure'; export type { ObjectInventory, QualifyResult,