@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g1822133
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/dist/PostgresBackendDriver.d.ts +48 -1
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-DlPBctw_.js → auth-users-columns-BfQHf9JE.js} +1222 -86
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-helpers.d.ts +57 -1
- package/dist/cli-output.d.ts +34 -0
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-CbvaGuVn.js} +170 -20
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1815 -1023
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
- package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
- package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
- package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- package/dist/schema/ensure-collection-tables.d.ts +61 -7
- package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +34 -7
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +9 -57
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +5 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +164 -23
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +124 -2
- package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +9 -8
- package/src/PostgresBackendDriver.ts +172 -6
- package/src/PostgresBootstrapper.ts +136 -11
- package/src/auth/ensure-tables.ts +212 -91
- package/src/auth/services.ts +82 -5
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +132 -13
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +371 -161
- package/src/collections/buildRegistry.ts +3 -1
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +142 -28
- package/src/history/ensure-history-table.ts +9 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +156 -77
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +374 -32
- package/src/schema/generate-drizzle-schema-logic.ts +152 -66
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +294 -16
- package/src/schema/generate-postgres-ddl.ts +38 -14
- package/src/schema/generated-schema-staleness.ts +171 -0
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +643 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +150 -7
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +253 -115
- package/src/services/PersistService.ts +82 -43
- package/src/services/RelationService.ts +37 -696
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +5 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +382 -118
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +433 -35
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/ensure-collection-policies-ViG8XiPn.js +0 -57
- package/dist/ensure-collection-policies-ViG8XiPn.js.map +0 -1
- package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
- package/dist/policy-CeA1JcxP.js +0 -105
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-DlPBctw_.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-B2LsrINK.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* no process.exit. It is imported by introspect-db.ts (the CLI entry-point)
|
|
7
7
|
* and consumed directly by tests.
|
|
8
8
|
*/
|
|
9
|
+
import { firstFreeKey, toWireKey } from "@rebasepro/utils";
|
|
9
10
|
import { inferPropertyFromData } from "./introspect-db-inference";
|
|
10
11
|
import { humanize } from "./introspect-db-naming";
|
|
11
12
|
import { mapPgType } from "./introspect-db-types";
|
|
@@ -218,7 +219,14 @@ export function singularize(word: string): string {
|
|
|
218
219
|
* e.g. "company_token" -> "companyTokenCollection"
|
|
219
220
|
*/
|
|
220
221
|
export function toCollectionVarName(tableName: string): string {
|
|
221
|
-
|
|
222
|
+
const camel = tableName.replace(/_([a-z])/g, (_g, letter: string) => letter.toUpperCase()) + "Collection";
|
|
223
|
+
// Only reshapes names that are not identifiers already, so every table that
|
|
224
|
+
// generated a working file keeps the exact variable name it had. A table
|
|
225
|
+
// called `2024 archive` used to emit `const 2024 archiveCollection`, which
|
|
226
|
+
// is three syntax errors rather than a declaration.
|
|
227
|
+
if (JS_IDENTIFIER.test(camel)) return camel;
|
|
228
|
+
const sanitized = camel.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
229
|
+
return /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized;
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
export function getIconForTable(tableName: string): string {
|
|
@@ -572,10 +580,61 @@ export interface GeneratedFile {
|
|
|
572
580
|
* have no database to read constraints or row counts from, and they must keep
|
|
573
581
|
* producing a valid collection.
|
|
574
582
|
*/
|
|
583
|
+
/**
|
|
584
|
+
* Which `defineCollection` — if any — the project being generated into can import.
|
|
585
|
+
*
|
|
586
|
+
* A bare `const x: PostgresCollectionConfig = { … }` annotation widens `properties`
|
|
587
|
+
* to `Record<string, …>`, and every key-shaped field in the admin block —
|
|
588
|
+
* `titleProperty`, `sort`, `propertiesOrder`, `listProperties`, `fixedFilter` — is
|
|
589
|
+
* derived from those keys. Annotated, they accept any string: introspection was
|
|
590
|
+
* emitting a `propertiesOrder` array that nothing checked, so renaming a column and
|
|
591
|
+
* re-introspecting left a stale key that compiled silently. `defineCollection` is
|
|
592
|
+
* the identity function whose `const P` type parameter keeps the keys literal, which
|
|
593
|
+
* is what turns that checking on.
|
|
594
|
+
*
|
|
595
|
+
* There are two of them and they are not interchangeable:
|
|
596
|
+
*
|
|
597
|
+
* - `admin-types` — `@rebasepro/admin-types`. Its index side-effect-imports
|
|
598
|
+
* `augment.ts`, so importing it is also what *declares* the `admin` block. Only a
|
|
599
|
+
* project that depends on the package can resolve it.
|
|
600
|
+
* - `common` — `@rebasepro/common`. Same key inference, no admin surface, no React
|
|
601
|
+
* anywhere in its graph (`scripts/headless-guard` lists it as core). This is the
|
|
602
|
+
* headless flavour.
|
|
603
|
+
* - `annotation` — neither package is declared, so neither import would resolve and
|
|
604
|
+
* the old annotation is the only honest thing to emit. Projects scaffolded before
|
|
605
|
+
* `@rebasepro/common` joined the headless config package land here.
|
|
606
|
+
*
|
|
607
|
+
* The last two emit **no admin block, on the collection or on any property**. That is
|
|
608
|
+
* not a downgrade: `@rebasepro/types` declares no `admin` field at all, so the block
|
|
609
|
+
* introspection used to emit was a type error in every headless project it was
|
|
610
|
+
* written into. See `packages/admin-types/src/augment.ts`.
|
|
611
|
+
*/
|
|
612
|
+
export type CollectionBuilder = "admin-types" | "common" | "annotation";
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The package specifiers the generated files name, spelled once.
|
|
616
|
+
*
|
|
617
|
+
* Written as constants rather than inline in the import templates below because
|
|
618
|
+
* `scripts/headless-guard/check-types.mjs` scans core sources for `from
|
|
619
|
+
* "@rebasepro/admin-types"` and cannot tell a real import from one this module
|
|
620
|
+
* *writes*. It is right to be that blunt — the guard's whole value is that it
|
|
621
|
+
* cannot be reasoned around — so the string simply never appears in that shape
|
|
622
|
+
* here. Inlining them back into the templates re-breaks `check:types-headless`.
|
|
623
|
+
*/
|
|
624
|
+
export const ADMIN_TYPES_PACKAGE = "@rebasepro/admin-types";
|
|
625
|
+
export const COMMON_PACKAGE = "@rebasepro/common";
|
|
626
|
+
export const TYPES_PACKAGE = "@rebasepro/types";
|
|
627
|
+
|
|
575
628
|
export interface GenerationContext {
|
|
576
629
|
metadata?: SchemaMetadata;
|
|
577
630
|
classifications?: Map<string, TableClassification>;
|
|
578
631
|
checkFacts?: CheckFactsByTable;
|
|
632
|
+
/**
|
|
633
|
+
* Defaults to `admin-types`, which is what the generator has always emitted.
|
|
634
|
+
* The CLI never relies on the default — `introspect-db.ts` detects the flavour
|
|
635
|
+
* from the target project and passes it. See `detectCollectionBuilder`.
|
|
636
|
+
*/
|
|
637
|
+
builder?: CollectionBuilder;
|
|
579
638
|
}
|
|
580
639
|
|
|
581
640
|
/** Adds entries to a property's `validation` block, creating it if absent. */
|
|
@@ -601,8 +660,17 @@ function hasGeneratedKey(extra: string, key: string): boolean {
|
|
|
601
660
|
return new RegExp(`(^|[\\s{,])${key}\\s*:`).test(extra);
|
|
602
661
|
}
|
|
603
662
|
|
|
604
|
-
/**
|
|
605
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Adds entries to a property's `admin` block, creating it if absent.
|
|
665
|
+
*
|
|
666
|
+
* `emitAdmin` is false for the headless flavours, where `BaseProperty` has no
|
|
667
|
+
* `admin` field to put them in — see {@link CollectionBuilder}. The options are
|
|
668
|
+
* dropped rather than relocated: every one of them (`readOnly`, `multiline`,
|
|
669
|
+
* `hideFromCollection`, `urlPreview`) describes a form widget, and there is no
|
|
670
|
+
* form.
|
|
671
|
+
*/
|
|
672
|
+
function withAdminOptions(extra: string, entries: string[], emitAdmin = true): string {
|
|
673
|
+
if (!emitAdmin) return extra;
|
|
606
674
|
if (entries.length === 0) return extra;
|
|
607
675
|
const block = entries.map((e) => ` ${e}`).join(",\n");
|
|
608
676
|
if (extra.includes("admin: {")) {
|
|
@@ -616,22 +684,35 @@ function quote(value: string): string {
|
|
|
616
684
|
return JSON.stringify(value);
|
|
617
685
|
}
|
|
618
686
|
|
|
687
|
+
const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
688
|
+
|
|
619
689
|
/**
|
|
620
|
-
*
|
|
690
|
+
* An object key for the generated file: verbatim when the name is a JavaScript
|
|
691
|
+
* identifier, quoted otherwise.
|
|
692
|
+
*
|
|
693
|
+
* Postgres constrains an identifier only by quoting, so `order`, `full name`
|
|
694
|
+
* and `2fa_enabled` are all ordinary column names — and all three produced a
|
|
695
|
+
* file that did not parse when written as a bare key.
|
|
621
696
|
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
* cannot
|
|
697
|
+
* Still needed now that keys are camel-cased rather than copied from the
|
|
698
|
+
* column: `toWireKey` splits on separators and joins, which fixes `full name`
|
|
699
|
+
* but cannot fix a name that is not an identifier for some other reason —
|
|
700
|
+
* `2fa_enabled` becomes `2faEnabled`, still leading with a digit, and `order`
|
|
701
|
+
* was never a separator problem at all.
|
|
625
702
|
*/
|
|
626
|
-
function
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
703
|
+
function propKey(name: string): string {
|
|
704
|
+
return JS_IDENTIFIER.test(name) ? name : quote(name);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Text safe to put after `//`.
|
|
709
|
+
*
|
|
710
|
+
* A table comment or a classification reason is free text out of the database.
|
|
711
|
+
* A newline in one ended the comment and let the rest of the value continue as
|
|
712
|
+
* code.
|
|
713
|
+
*/
|
|
714
|
+
function commentText(value: string): string {
|
|
715
|
+
return value.replace(/\s*[\r\n]+\s*/g, " ");
|
|
635
716
|
}
|
|
636
717
|
|
|
637
718
|
/** A property-key array, one key per line, indented for the admin block. */
|
|
@@ -674,7 +755,16 @@ export function generateCollectionFile(
|
|
|
674
755
|
.map((u) => u.column_names[0])
|
|
675
756
|
);
|
|
676
757
|
|
|
677
|
-
const
|
|
758
|
+
const builder: CollectionBuilder = context.builder ?? "admin-types";
|
|
759
|
+
/** Whether the target project has an `admin` field to write into at all. */
|
|
760
|
+
const emitAdmin = builder === "admin-types";
|
|
761
|
+
|
|
762
|
+
const BUILDER_IMPORT: Record<CollectionBuilder, string> = {
|
|
763
|
+
"admin-types": `import { defineCollection } from ${quote(ADMIN_TYPES_PACKAGE)};`,
|
|
764
|
+
common: `import { defineCollection } from ${quote(COMMON_PACKAGE)};`,
|
|
765
|
+
annotation: `import { PostgresCollectionConfig } from ${quote(TYPES_PACKAGE)};`
|
|
766
|
+
};
|
|
767
|
+
const imports = new Set<string>([BUILDER_IMPORT[builder]]);
|
|
678
768
|
|
|
679
769
|
/**
|
|
680
770
|
* Imports the collection a relation points at — unless it is this one.
|
|
@@ -688,14 +778,48 @@ export function generateCollectionFile(
|
|
|
688
778
|
*/
|
|
689
779
|
const importCollection = (otherTable: string): string => {
|
|
690
780
|
const varName = toCollectionVarName(otherTable);
|
|
691
|
-
if (otherTable !== tableName) imports.add(`import ${varName} from
|
|
781
|
+
if (otherTable !== tableName) imports.add(`import ${varName} from ${quote(`./${otherTable}`)};`);
|
|
692
782
|
return varName;
|
|
693
783
|
};
|
|
694
784
|
|
|
785
|
+
/**
|
|
786
|
+
* A relation's `target` thunk, with its return type spelled out.
|
|
787
|
+
*
|
|
788
|
+
* The annotation is what makes `defineCollection` survive a relational schema.
|
|
789
|
+
* Without an explicit type on the const, the collection's type is *inferred*,
|
|
790
|
+
* and a relation cycle — `posts` belongs to `authors`, `authors` has many
|
|
791
|
+
* `posts`; or `employees.reports_to -> employees`, which northwind, chinook and
|
|
792
|
+
* musicbrainz all have — makes that inference circular: `TS7022: implicitly has
|
|
793
|
+
* type 'any' because it is referenced directly or indirectly in its own
|
|
794
|
+
* initializer`, plus `TS7023` on the thunk and `TS2303` on the import alias.
|
|
795
|
+
* Naming the return type lets the checker type the thunk without resolving the
|
|
796
|
+
* collection it points at, which breaks the cycle. Nothing else is given up:
|
|
797
|
+
* the inference that matters runs over `properties`, not over `relations`.
|
|
798
|
+
*
|
|
799
|
+
* The annotated flavour has no cycle to break — its const is already typed — so
|
|
800
|
+
* it keeps the plainer thunk it has always emitted.
|
|
801
|
+
*/
|
|
802
|
+
const relationTarget = (targetVarName: string): string => {
|
|
803
|
+
if (builder === "annotation") return `() => ${targetVarName}`;
|
|
804
|
+
imports.add(`import type { AnyCollectionConfig } from ${quote(TYPES_PACKAGE)};`);
|
|
805
|
+
return `(): AnyCollectionConfig => ${targetVarName}`;
|
|
806
|
+
};
|
|
807
|
+
|
|
695
808
|
let propsOutput = "";
|
|
696
809
|
let relationsOutput = "";
|
|
697
810
|
const orderEntries: PropertyOrderEntry[] = [];
|
|
698
811
|
const propertyBlocks = new Map<string, string>();
|
|
812
|
+
/**
|
|
813
|
+
* Column → the property key it was generated under.
|
|
814
|
+
*
|
|
815
|
+
* Needed because the structural helpers below (`deriveTitleProperty`,
|
|
816
|
+
* `deriveKanbanProperty`, `deriveSort`) answer in *columns* — they read
|
|
817
|
+
* `pg_attribute` — while `display.title`, `kanban.columnProperty` and
|
|
818
|
+
* `sort` name **properties**. The two used to be the same string, so
|
|
819
|
+
* nothing carried the translation; now `full_name` is generated as
|
|
820
|
+
* `fullName` and a title pointing at `full_name` points at nothing.
|
|
821
|
+
*/
|
|
822
|
+
const keyByColumn = new Map<string, string>();
|
|
699
823
|
/** Properties the list view will not render, so `listProperties` skips them. */
|
|
700
824
|
const hiddenFromCollection = new Set<string>();
|
|
701
825
|
let columnIndex = 0;
|
|
@@ -711,6 +835,23 @@ export function generateCollectionFile(
|
|
|
711
835
|
|
|
712
836
|
const currentIndex = columnIndex++;
|
|
713
837
|
|
|
838
|
+
// The key this column is generated under — its *wire* name, which is
|
|
839
|
+
// not its column name. `columnName` below carries the column, so the
|
|
840
|
+
// two never have to agree and the API stops carrying `user_id` next to
|
|
841
|
+
// `displayName`.
|
|
842
|
+
//
|
|
843
|
+
// Camel-casing makes collisions possible where none existed: `user_id`
|
|
844
|
+
// and `userId` are two columns and one key, which is a duplicate key in
|
|
845
|
+
// an object literal — a TypeScript error that stops the whole generated
|
|
846
|
+
// collection compiling. Resolved the same way the foreign-key loop
|
|
847
|
+
// below resolves its own: first free candidate, then a numbered tail,
|
|
848
|
+
// so no column is ever dropped. The raw column name is the second
|
|
849
|
+
// candidate, so the loser of a collision still gets a name that means
|
|
850
|
+
// something. Deterministic for a given database: the columns arrive in
|
|
851
|
+
// ordinal order, so the same schema always yields the same keys.
|
|
852
|
+
const propertyKey = firstFreeKey([toWireKey(col.column_name), col.column_name], propertyBlocks);
|
|
853
|
+
keyByColumn.set(col.column_name, propertyKey);
|
|
854
|
+
|
|
714
855
|
// Check if this column uses a PostgreSQL enum type
|
|
715
856
|
const colEnumValues = enumMap.get(col.udt_name);
|
|
716
857
|
const isEnumColumn = col.data_type === "USER-DEFINED" && colEnumValues !== undefined;
|
|
@@ -727,7 +868,7 @@ export function generateCollectionFile(
|
|
|
727
868
|
|
|
728
869
|
if (!isEnumColumn && sampleData && sampleData.length > 0) {
|
|
729
870
|
const values = sampleData.map(r => r[col.column_name]);
|
|
730
|
-
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name));
|
|
871
|
+
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);
|
|
731
872
|
if (inferred.propType) finalPropType = inferred.propType;
|
|
732
873
|
if (inferred.extra) inferenceExtra = inferred.extra;
|
|
733
874
|
}
|
|
@@ -737,7 +878,7 @@ export function generateCollectionFile(
|
|
|
737
878
|
// Enum values — generate real enum from the PG enum
|
|
738
879
|
if (isEnumColumn && colEnumValues) {
|
|
739
880
|
const enumEntries = colEnumValues
|
|
740
|
-
.map((v) => `{ id:
|
|
881
|
+
.map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`)
|
|
741
882
|
.join(", ");
|
|
742
883
|
extra += `\n enum: [${enumEntries}],`;
|
|
743
884
|
} else if (columnChecks?.enumValues && !inferenceExtra.includes("enum:") && propType === "string") {
|
|
@@ -754,13 +895,16 @@ export function generateCollectionFile(
|
|
|
754
895
|
// Date auto-value heuristics
|
|
755
896
|
if (finalPropType === "date") {
|
|
756
897
|
if (colNameLower === "created_at" || colNameLower === "createdat") {
|
|
757
|
-
extra += "\n autoValue: \"on_create\"
|
|
758
|
-
|
|
898
|
+
extra += "\n autoValue: \"on_create\",";
|
|
899
|
+
extra = withAdminOptions(extra, ["readOnly: true", "hideFromCollection: true"], emitAdmin);
|
|
900
|
+
hiddenFromCollection.add(propertyKey);
|
|
759
901
|
} else if (colNameLower === "updated_at" || colNameLower === "updatedat") {
|
|
760
|
-
extra += "\n autoValue: \"on_update\"
|
|
761
|
-
|
|
902
|
+
extra += "\n autoValue: \"on_update\",";
|
|
903
|
+
extra = withAdminOptions(extra, ["readOnly: true", "hideFromCollection: true"], emitAdmin);
|
|
904
|
+
hiddenFromCollection.add(propertyKey);
|
|
762
905
|
} else if (col.column_default && (col.column_default.includes("now()") || col.column_default.includes("CURRENT_TIMESTAMP"))) {
|
|
763
|
-
extra += "\n autoValue: \"on_create\"
|
|
906
|
+
extra += "\n autoValue: \"on_create\",";
|
|
907
|
+
extra = withAdminOptions(extra, ["readOnly: true"], emitAdmin);
|
|
764
908
|
}
|
|
765
909
|
}
|
|
766
910
|
|
|
@@ -776,9 +920,9 @@ export function generateCollectionFile(
|
|
|
776
920
|
else if (innerType === "boolean") colType = "boolean[]";
|
|
777
921
|
}
|
|
778
922
|
if (colType) {
|
|
779
|
-
extra += `\n columnType:
|
|
923
|
+
extra += `\n columnType: ${quote(colType)},`;
|
|
780
924
|
}
|
|
781
|
-
extra += `\n of: { name:
|
|
925
|
+
extra += `\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(innerType)} },`;
|
|
782
926
|
} else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) {
|
|
783
927
|
extra += "\n keyValue: true,";
|
|
784
928
|
}
|
|
@@ -789,20 +933,20 @@ export function generateCollectionFile(
|
|
|
789
933
|
const isMedia = colNameLower.includes("image") || colNameLower.includes("avatar") || colNameLower.includes("photo") || colNameLower.includes("logo") || colNameLower.includes("cover");
|
|
790
934
|
|
|
791
935
|
if (isMedia) {
|
|
792
|
-
extra += `\n storage: {\n storagePath:
|
|
936
|
+
extra += `\n storage: {\n storagePath: ${quote(`${tableName}/${col.column_name}`)}\n },`;
|
|
793
937
|
} else if (isUrl) {
|
|
794
938
|
extra += "\n url: true,";
|
|
795
939
|
} else if (colNameLower === "description" || colNameLower === "summary" || colNameLower === "excerpt") {
|
|
796
|
-
extra
|
|
940
|
+
extra = withAdminOptions(extra, ["multiline: true"], emitAdmin);
|
|
797
941
|
} else if (colNameLower === "content" || colNameLower === "body") {
|
|
798
942
|
// Inside `admin`, because that is where both options live. At the
|
|
799
943
|
// top of the property — where these were — the generated file
|
|
800
944
|
// does not compile: `StringProperty` declares neither. Six of
|
|
801
945
|
// OpenStreetMap's tables have a `body` column, which is how this
|
|
802
946
|
// surfaced.
|
|
803
|
-
extra
|
|
947
|
+
extra = withAdminOptions(extra, ["multiline: true", "markdown: true"], emitAdmin);
|
|
804
948
|
} else if (col.data_type === "text") {
|
|
805
|
-
extra
|
|
949
|
+
extra = withAdminOptions(extra, ["multiline: true"], emitAdmin);
|
|
806
950
|
}
|
|
807
951
|
}
|
|
808
952
|
|
|
@@ -857,9 +1001,9 @@ export function generateCollectionFile(
|
|
|
857
1001
|
const options = ["readOnly: true"];
|
|
858
1002
|
if (isDerivedIndexColumn(col) && !hasGeneratedKey(extra, "hideFromCollection")) {
|
|
859
1003
|
options.push("hideFromCollection: true");
|
|
860
|
-
hiddenFromCollection.add(
|
|
1004
|
+
hiddenFromCollection.add(propertyKey);
|
|
861
1005
|
}
|
|
862
|
-
extra = withAdminOptions(extra, options);
|
|
1006
|
+
extra = withAdminOptions(extra, options, emitAdmin);
|
|
863
1007
|
}
|
|
864
1008
|
|
|
865
1009
|
// `COMMENT ON COLUMN` — documentation the author already wrote, which
|
|
@@ -872,7 +1016,7 @@ export function generateCollectionFile(
|
|
|
872
1016
|
// Identify IDs (unless already inferred as UUID/CUID by inferenceEngine)
|
|
873
1017
|
if (meta.pks.includes(col.column_name)) {
|
|
874
1018
|
if (isCompositePk) {
|
|
875
|
-
extra += `\n // Part of composite primary key (${meta.pks.join(", ")})`;
|
|
1019
|
+
extra += `\n // Part of composite primary key (${commentText(meta.pks.join(", "))})`;
|
|
876
1020
|
} else if (finalPropType === "number" && !inferenceExtra.includes("isId:")) {
|
|
877
1021
|
extra += "\n isId: \"increment\",";
|
|
878
1022
|
} else if (col.data_type.toLowerCase() === "uuid" && !inferenceExtra.includes("isId:")) {
|
|
@@ -901,7 +1045,7 @@ export function generateCollectionFile(
|
|
|
901
1045
|
const humanName = humanize(col.column_name);
|
|
902
1046
|
|
|
903
1047
|
orderEntries.push({
|
|
904
|
-
key:
|
|
1048
|
+
key: propertyKey,
|
|
905
1049
|
ctx: {
|
|
906
1050
|
propType: finalPropType,
|
|
907
1051
|
isPk: meta.pks.includes(col.column_name),
|
|
@@ -912,11 +1056,11 @@ export function generateCollectionFile(
|
|
|
912
1056
|
}
|
|
913
1057
|
});
|
|
914
1058
|
|
|
915
|
-
propertyBlocks.set(
|
|
916
|
-
${
|
|
917
|
-
name:
|
|
918
|
-
columnName:
|
|
919
|
-
type:
|
|
1059
|
+
propertyBlocks.set(propertyKey, `
|
|
1060
|
+
${propKey(propertyKey)}: {
|
|
1061
|
+
name: ${quote(humanName)},
|
|
1062
|
+
columnName: ${quote(col.column_name)},
|
|
1063
|
+
type: ${quote(finalPropType)},${extra}
|
|
920
1064
|
},`);
|
|
921
1065
|
}
|
|
922
1066
|
|
|
@@ -936,11 +1080,17 @@ export function generateCollectionFile(
|
|
|
936
1080
|
// every foreign key after the table it points at — `area_tag (area,
|
|
937
1081
|
// tag)` — so 67 of its 339 collections came out with a property
|
|
938
1082
|
// declared twice.
|
|
1083
|
+
//
|
|
1084
|
+
// Camel-cased for the same reason the columns above are: this is a
|
|
1085
|
+
// property key, it sits in the same object literal, and a
|
|
1086
|
+
// `blog_author` beside a `publishedAt` is the two-conventions
|
|
1087
|
+
// defect reproduced inside a single collection.
|
|
1088
|
+
const stripped = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
939
1089
|
const relName = firstFreeKey(
|
|
940
1090
|
[
|
|
941
|
-
|
|
942
|
-
targetTableName,
|
|
943
|
-
`${
|
|
1091
|
+
stripped,
|
|
1092
|
+
toWireKey(targetTableName),
|
|
1093
|
+
`${stripped}Relation`
|
|
944
1094
|
],
|
|
945
1095
|
propertyBlocks
|
|
946
1096
|
);
|
|
@@ -962,14 +1112,14 @@ export function generateCollectionFile(
|
|
|
962
1112
|
const relHumanName = humanize(relName);
|
|
963
1113
|
|
|
964
1114
|
propertyBlocks.set(relName, `
|
|
965
|
-
${relName}: {
|
|
966
|
-
name:
|
|
1115
|
+
${propKey(relName)}: {
|
|
1116
|
+
name: ${quote(relHumanName)},
|
|
967
1117
|
type: "relation",
|
|
968
|
-
// mapped from foreign key: ${fk.column_name} -> ${targetTableName}(${fk.foreign_column_name})
|
|
1118
|
+
// mapped from foreign key: ${commentText(fk.column_name)} -> ${commentText(targetTableName)}(${commentText(fk.foreign_column_name)})
|
|
969
1119
|
relation: {
|
|
970
1120
|
kind: "belongsTo",
|
|
971
|
-
target:
|
|
972
|
-
localKey:
|
|
1121
|
+
target: ${relationTarget(targetCollectionCamel)},
|
|
1122
|
+
localKey: ${quote(fk.column_name)}
|
|
973
1123
|
}
|
|
974
1124
|
},`);
|
|
975
1125
|
}
|
|
@@ -986,10 +1136,10 @@ export function generateCollectionFile(
|
|
|
986
1136
|
relationsOutput += `
|
|
987
1137
|
{
|
|
988
1138
|
kind: "hasMany",
|
|
989
|
-
relationName:
|
|
990
|
-
target:
|
|
991
|
-
// the ${sourceTableName}.${fk.column_name} FK points back here
|
|
992
|
-
foreignKeyOnTarget:
|
|
1139
|
+
relationName: ${quote(sourceTableName)},
|
|
1140
|
+
target: ${relationTarget(targetCollectionCamel)},
|
|
1141
|
+
// the ${commentText(sourceTableName)}.${commentText(fk.column_name)} FK points back here
|
|
1142
|
+
foreignKeyOnTarget: ${quote(fk.column_name)}
|
|
993
1143
|
},`;
|
|
994
1144
|
}
|
|
995
1145
|
|
|
@@ -1018,12 +1168,12 @@ export function generateCollectionFile(
|
|
|
1018
1168
|
relationsOutput += `
|
|
1019
1169
|
{
|
|
1020
1170
|
kind: "manyToMany",
|
|
1021
|
-
relationName:
|
|
1022
|
-
target:
|
|
1171
|
+
relationName: ${quote(relPropName)},
|
|
1172
|
+
target: ${relationTarget(toCollectionVarName(tableName))},
|
|
1023
1173
|
through: {
|
|
1024
|
-
table:
|
|
1025
|
-
sourceColumn:
|
|
1026
|
-
targetColumn:
|
|
1174
|
+
table: ${quote(jt)},
|
|
1175
|
+
sourceColumn: ${quote(thisFk.column_name)},
|
|
1176
|
+
targetColumn: ${quote(otherFk.column_name)}
|
|
1027
1177
|
}
|
|
1028
1178
|
},`;
|
|
1029
1179
|
continue;
|
|
@@ -1045,14 +1195,14 @@ export function generateCollectionFile(
|
|
|
1045
1195
|
const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);
|
|
1046
1196
|
|
|
1047
1197
|
const throughCode = thisFk
|
|
1048
|
-
? `\n through: {\n table:
|
|
1198
|
+
? `\n through: {\n table: ${quote(jt)},\n sourceColumn: ${quote(thisFk.column_name)},\n targetColumn: ${quote(otherFk.column_name)}\n }`
|
|
1049
1199
|
: "";
|
|
1050
1200
|
|
|
1051
1201
|
relationsOutput += `
|
|
1052
1202
|
{
|
|
1053
1203
|
kind: "manyToMany",
|
|
1054
|
-
relationName:
|
|
1055
|
-
target:
|
|
1204
|
+
relationName: ${quote(targetTableName)},
|
|
1205
|
+
target: ${relationTarget(targetCollectionCamel)},${throughCode}
|
|
1056
1206
|
},`;
|
|
1057
1207
|
}
|
|
1058
1208
|
}
|
|
@@ -1071,7 +1221,7 @@ export function generateCollectionFile(
|
|
|
1071
1221
|
// config, where they have not belonged since the admin block was split out:
|
|
1072
1222
|
// `PostgresCollectionConfig` does not declare them, so every generated file
|
|
1073
1223
|
// was a type error, and the panel — which reads the block — never saw them.
|
|
1074
|
-
const adminEntries: string[] = [`icon:
|
|
1224
|
+
const adminEntries: string[] = [`icon: ${quote(icon)}`];
|
|
1075
1225
|
|
|
1076
1226
|
if (classification) {
|
|
1077
1227
|
const derivedFacts = context.metadata
|
|
@@ -1088,14 +1238,17 @@ export function generateCollectionFile(
|
|
|
1088
1238
|
}
|
|
1089
1239
|
|
|
1090
1240
|
if (derivedFacts) {
|
|
1241
|
+
// Each of these comes back as a column and is emitted as a property.
|
|
1242
|
+
const asProperty = (column: string): string => keyByColumn.get(column) ?? toWireKey(column);
|
|
1243
|
+
|
|
1091
1244
|
const titleProperty = deriveTitleProperty(derivedFacts);
|
|
1092
|
-
if (titleProperty) adminEntries.push(`
|
|
1245
|
+
if (titleProperty) adminEntries.push(`display: { title: ${quote(asProperty(titleProperty))} }`);
|
|
1093
1246
|
|
|
1094
1247
|
const kanbanProperty = deriveKanbanProperty(derivedFacts);
|
|
1095
|
-
if (kanbanProperty) adminEntries.push(`kanban: {\n columnProperty: ${quote(kanbanProperty)}\n }`);
|
|
1248
|
+
if (kanbanProperty) adminEntries.push(`kanban: {\n columnProperty: ${quote(asProperty(kanbanProperty))}\n }`);
|
|
1096
1249
|
|
|
1097
1250
|
const sort = deriveSort(derivedFacts);
|
|
1098
|
-
if (sort) adminEntries.push(`sort: [${quote(sort[0])}, "desc"]`);
|
|
1251
|
+
if (sort) adminEntries.push(`sort: [${quote(asProperty(sort[0]))}, "desc"]`);
|
|
1099
1252
|
}
|
|
1100
1253
|
|
|
1101
1254
|
const listProperties = deriveListProperties(sortedPropertiesOrder, hiddenFromCollection);
|
|
@@ -1106,7 +1259,14 @@ export function generateCollectionFile(
|
|
|
1106
1259
|
|
|
1107
1260
|
adminEntries.push(`propertiesOrder: ${formatKeyList(sortedPropertiesOrder)}`);
|
|
1108
1261
|
|
|
1109
|
-
|
|
1262
|
+
// Every entry above names a property key, and with `defineCollection` those
|
|
1263
|
+
// keys are now checked against `properties` — a stale `propertiesOrder` entry
|
|
1264
|
+
// left behind by a renamed column is a compile error rather than a silent
|
|
1265
|
+
// no-op. Which is also why the block cannot be emitted where the field is not
|
|
1266
|
+
// declared: see {@link CollectionBuilder}.
|
|
1267
|
+
const adminBlock = emitAdmin
|
|
1268
|
+
? `\n admin: {\n ${adminEntries.join(",\n ")}\n }`
|
|
1269
|
+
: "";
|
|
1110
1270
|
|
|
1111
1271
|
const descriptionBlock = tableComment
|
|
1112
1272
|
? `\n description: ${quote(tableComment)},`
|
|
@@ -1116,20 +1276,36 @@ export function generateCollectionFile(
|
|
|
1116
1276
|
// decision the reader may disagree with. Naming the evidence tells them
|
|
1117
1277
|
// which line to delete when they do.
|
|
1118
1278
|
const classificationNote = classification && classification.role !== "entity"
|
|
1119
|
-
? `\n// Introspected as a ${classification.role}: ${classification.reason}.\n`
|
|
1279
|
+
? `\n// Introspected as a ${commentText(classification.role)}: ${commentText(classification.reason)}.\n`
|
|
1120
1280
|
: "";
|
|
1121
1281
|
|
|
1122
1282
|
const collectionVarName = toCollectionVarName(tableName);
|
|
1123
|
-
const
|
|
1283
|
+
// `const x = defineCollection({ … })` is also the shape the ts-morph schema
|
|
1284
|
+
// editor in `@rebasepro/server` expects — `COLLECTION_FACTORIES` — so an
|
|
1285
|
+
// introspected collection is now editable from the panel the way a scaffolded
|
|
1286
|
+
// one is.
|
|
1287
|
+
const [open, close] = builder === "annotation"
|
|
1288
|
+
? [`const ${collectionVarName}: PostgresCollectionConfig = {`, "};"]
|
|
1289
|
+
: [`const ${collectionVarName} = defineCollection({`, "});"];
|
|
1290
|
+
// Package imports first, then siblings. `AnyCollectionConfig` is added the
|
|
1291
|
+
// moment the first relation needs it — which is after the sibling collections
|
|
1292
|
+
// it points at have already been added — and only when a relation needs it, so
|
|
1293
|
+
// a project with `noUnusedLocals` never sees an import it does not use.
|
|
1294
|
+
const importLines = Array.from(imports);
|
|
1295
|
+
const orderedImports = [
|
|
1296
|
+
...importLines.filter((line) => !line.includes('from "./')),
|
|
1297
|
+
...importLines.filter((line) => line.includes('from "./'))
|
|
1298
|
+
];
|
|
1299
|
+
const fileContent = `${orderedImports.join("\n")}
|
|
1124
1300
|
${classificationNote}
|
|
1125
|
-
|
|
1126
|
-
name:
|
|
1127
|
-
singularName:
|
|
1128
|
-
slug:
|
|
1129
|
-
table:
|
|
1301
|
+
${open}
|
|
1302
|
+
name: ${quote(collectionName)},
|
|
1303
|
+
singularName: ${quote(singular)},
|
|
1304
|
+
slug: ${quote(tableName)},
|
|
1305
|
+
table: ${quote(tableName)},${descriptionBlock}
|
|
1130
1306
|
properties: {${propsOutput}
|
|
1131
1307
|
},${relationsBlock}${adminBlock}
|
|
1132
|
-
}
|
|
1308
|
+
${close}
|
|
1133
1309
|
|
|
1134
1310
|
export default ${collectionVarName};
|
|
1135
1311
|
`;
|
|
@@ -1146,7 +1322,7 @@ export function generateIndexContent(fileNames: string[]): string {
|
|
|
1146
1322
|
let arrayElements = "";
|
|
1147
1323
|
for (const f of sorted) {
|
|
1148
1324
|
const varName = toCollectionVarName(f);
|
|
1149
|
-
imports += `import ${varName} from
|
|
1325
|
+
imports += `import ${varName} from ${quote(`./${f}`)};\n`;
|
|
1150
1326
|
arrayElements += ` ${varName},\n`;
|
|
1151
1327
|
}
|
|
1152
1328
|
return `${imports}\nexport const collections = [\n${arrayElements}];\n`;
|
|
@@ -1168,7 +1344,7 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
|
|
|
1168
1344
|
for (const f of sorted) {
|
|
1169
1345
|
if (!existingImports.has(f)) {
|
|
1170
1346
|
const varName = toCollectionVarName(f);
|
|
1171
|
-
newImports += `import ${varName} from
|
|
1347
|
+
newImports += `import ${varName} from ${quote(`./${f}`)};\n`;
|
|
1172
1348
|
newElements += ` ${varName},\n`;
|
|
1173
1349
|
}
|
|
1174
1350
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the project being introspected into can actually import.
|
|
3
|
+
*
|
|
4
|
+
* The generator is pure — `introspect-db-logic.ts` reads no files — so the one
|
|
5
|
+
* question it cannot answer for itself lives here: which `defineCollection`, if
|
|
6
|
+
* any, will resolve from the directory the collections are written to.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
import { ADMIN_TYPES_PACKAGE, COMMON_PACKAGE, type CollectionBuilder } from "./introspect-db-logic";
|
|
12
|
+
|
|
13
|
+
/** How far up to look before giving up, if no project root announces itself. */
|
|
14
|
+
const MAX_LEVELS = 8;
|
|
15
|
+
|
|
16
|
+
/** Every dependency name a manifest declares, in any of the three fields. */
|
|
17
|
+
function declaredDependencies(manifestPath: string): Set<string> {
|
|
18
|
+
const names = new Set<string>();
|
|
19
|
+
let parsed: unknown;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
22
|
+
} catch {
|
|
23
|
+
// A malformed or unreadable package.json is not this function's problem
|
|
24
|
+
// to report — it answers "can this import resolve", and the answer is no.
|
|
25
|
+
return names;
|
|
26
|
+
}
|
|
27
|
+
if (typeof parsed !== "object" || parsed === null) return names;
|
|
28
|
+
const manifest = parsed as Record<string, unknown>;
|
|
29
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
30
|
+
const block = manifest[field];
|
|
31
|
+
if (typeof block === "object" && block !== null) {
|
|
32
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return names;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Which `defineCollection` to generate against, for collections written to `outDir`.
|
|
40
|
+
*
|
|
41
|
+
* **The detection is the package manifests above the output directory**, unioned from
|
|
42
|
+
* `outDir` up to and including the project root — the first ancestor holding a
|
|
43
|
+
* `rebase.json`, or `MAX_LEVELS` up if there is none.
|
|
44
|
+
*
|
|
45
|
+
* That is the rule Node itself applies: a file in `config/collections` resolves a bare
|
|
46
|
+
* specifier through `config/node_modules`, then `<project>/node_modules`, and so on up.
|
|
47
|
+
* Reading the manifests along that same path answers the only question that matters —
|
|
48
|
+
* *will this import resolve in the project I am writing into* — from the state on disk
|
|
49
|
+
* at the moment of generation. The alternatives are all proxies for it: `rebase.json`'s
|
|
50
|
+
* `apps` block says a CMS scaffold declared an admin app, and a `frontend/` directory
|
|
51
|
+
* says one was scaffolded, but neither is what the compiler consults, and either can be
|
|
52
|
+
* true of a project whose `config` package does not depend on `@rebasepro/admin-types`.
|
|
53
|
+
*
|
|
54
|
+
* Ambiguity resolves towards the admin panel: a project that declares both packages has
|
|
55
|
+
* a panel, and `@rebasepro/admin-types` is the flavour that keeps the `admin` block.
|
|
56
|
+
*/
|
|
57
|
+
export function detectCollectionBuilder(outDir: string): CollectionBuilder {
|
|
58
|
+
let dir = path.resolve(outDir);
|
|
59
|
+
const declared = new Set<string>();
|
|
60
|
+
|
|
61
|
+
for (let level = 0; level < MAX_LEVELS; level++) {
|
|
62
|
+
const manifest = path.join(dir, "package.json");
|
|
63
|
+
if (fs.existsSync(manifest)) {
|
|
64
|
+
for (const name of declaredDependencies(manifest)) declared.add(name);
|
|
65
|
+
}
|
|
66
|
+
// The project root, and the last level worth reading: anything above it
|
|
67
|
+
// belongs to whatever the project happens to be checked out inside.
|
|
68
|
+
if (fs.existsSync(path.join(dir, "rebase.json"))) break;
|
|
69
|
+
|
|
70
|
+
const parent = path.dirname(dir);
|
|
71
|
+
if (parent === dir) break;
|
|
72
|
+
dir = parent;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (declared.has(ADMIN_TYPES_PACKAGE)) return "admin-types";
|
|
76
|
+
if (declared.has(COMMON_PACKAGE)) return "common";
|
|
77
|
+
return "annotation";
|
|
78
|
+
}
|