1
0
Fork 0

chore(payload): regenerate types and schema, harden sequence reset

- Regenerate payload-types and the drizzle schema for the new collections and
  fields (promotion nominations, leadership transfers, the accessory asset
  type, the Ornith model settings) and register the new migrations.
- Fix the reset-sequences SQL function to skip empty tables, and update the
  prod sync script to match.
This commit is contained in:
Jason Fraley 2026-10-03 03:30:43 -04:00
parent 35150e73ac
commit d9253e6725
6 changed files with 556 additions and 23 deletions

View file

@ -1,7 +1,8 @@
SELECT SELECT
'SELECT SETVAL(' || 'SELECT SETVAL(' ||
quote_literal(quote_ident(sequence_namespace.nspname) || '.' || quote_ident(class_sequence.relname)) || quote_literal(quote_ident(sequence_namespace.nspname) || '.' || quote_ident(class_sequence.relname)) ||
', COALESCE(MAX(' ||quote_ident(pg_attribute.attname)|| '), 1) ) FROM ' || ', COALESCE(MAX(' || quote_ident(pg_attribute.attname) || '), 1), ' ||
'MAX(' || quote_ident(pg_attribute.attname) || ') IS NOT NULL) FROM ' ||
quote_ident(table_namespace.nspname) || '.' || quote_ident(class_table.relname) || ';' quote_ident(table_namespace.nspname) || '.' || quote_ident(class_table.relname) || ';'
FROM pg_depend FROM pg_depend
INNER JOIN pg_class AS class_sequence INNER JOIN pg_class AS class_sequence

View file

@ -317,13 +317,34 @@ restore_ai_instructions() {
|| warn "Failed to reload ${AI_INSTRUCTIONS_IMAGES_TABLE} (non-fatal)" || warn "Failed to reload ${AI_INSTRUCTIONS_IMAGES_TABLE} (non-fatal)"
fi fi
PGPASSWORD="$TARGET_DB_PASS" psql \ ok "AI Compose prompts preserved (no regeneration needed on next boot)"
}
reset_sequences() {
info "Resetting sequences..."
local has_function
has_function=$(PGPASSWORD="$TARGET_DB_PASS" psql \
-h "$TARGET_DB_HOST" -p "$TARGET_DB_PORT" -U "$TARGET_DB_USER" \ -h "$TARGET_DB_HOST" -p "$TARGET_DB_PORT" -U "$TARGET_DB_USER" \
-d "$TARGET_DB_NAME" \ -d "$TARGET_DB_NAME" \
-c "SELECT setval(pg_get_serial_sequence('${AI_INSTRUCTIONS_TABLE}', 'id'), GREATEST(COALESCE((SELECT MAX(id) FROM ${AI_INSTRUCTIONS_TABLE}), 1), 1));" >/dev/null 2>&1 \ -tAc "SELECT to_regclass('maintenance.reset_owned_sequences(text)') IS NOT NULL;" 2>/dev/null)
|| true
ok "AI Compose prompts preserved (no regeneration needed on next boot)" if [[ "$has_function" != "t" ]]; then
warn "maintenance.reset_owned_sequences not available (migrations skipped or not applied); skipping sequence reset"
return 0
fi
if ! PGPASSWORD="$TARGET_DB_PASS" psql \
-h "$TARGET_DB_HOST" \
-p "$TARGET_DB_PORT" \
-U "$TARGET_DB_USER" \
-d "$TARGET_DB_NAME" \
-v ON_ERROR_STOP=1 \
-c "SELECT * FROM maintenance.reset_owned_sequences('public');"; then
die "Failed to reset sequences"
fi
ok "Sequences reset"
} }
sync_database() { sync_database() {
@ -395,15 +416,8 @@ sync_database() {
fi fi
fi fi
info "Resetting sequences..."
PGPASSWORD="$TARGET_DB_PASS" psql \
-h "$TARGET_DB_HOST" \
-p "$TARGET_DB_PORT" \
-U "$TARGET_DB_USER" \
-d "$TARGET_DB_NAME" \
-c "SELECT setval(pg_get_serial_sequence(t.table_name, c.column_name), (SELECT COALESCE(MAX(c.column_name::text::bigint), 1) FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name WHERE c.column_name = 'id')) FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name WHERE c.column_name = 'id' AND c.column_default LIKE 'nextval%' AND t.table_schema = 'public';" 2>/dev/null || true
restore_ai_instructions restore_ai_instructions
reset_sequences
ok "Database sync complete: ${TARGET_DB_NAME}" ok "Database sync complete: ${TARGET_DB_NAME}"
} }

View file

@ -0,0 +1,91 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE SCHEMA IF NOT EXISTS maintenance;
CREATE OR REPLACE FUNCTION maintenance.reset_owned_sequences(
target_schema text DEFAULT 'public'
)
RETURNS TABLE (
sequence_name text,
table_name text,
column_name text,
previous_value bigint,
new_value bigint,
is_called boolean
)
LANGUAGE plpgsql
AS $$
DECLARE
sequence_record record;
max_value bigint;
prior_value bigint;
should_be_called boolean;
BEGIN
FOR sequence_record IN
SELECT
sequence_namespace.nspname AS sequence_schema,
class_sequence.relname AS sequence_name,
table_namespace.nspname AS table_schema,
class_table.relname AS table_name,
pg_attribute.attname AS column_name
FROM pg_depend
INNER JOIN pg_class AS class_sequence
ON class_sequence.oid = pg_depend.objid
AND class_sequence.relkind = 'S'
INNER JOIN pg_class AS class_table
ON class_table.oid = pg_depend.refobjid
INNER JOIN pg_attribute
ON pg_attribute.attrelid = class_table.oid
AND pg_depend.refobjsubid = pg_attribute.attnum
INNER JOIN pg_namespace AS table_namespace
ON table_namespace.oid = class_table.relnamespace
INNER JOIN pg_namespace AS sequence_namespace
ON sequence_namespace.oid = class_sequence.relnamespace
WHERE table_namespace.nspname = target_schema
AND pg_depend.deptype IN ('a', 'i')
ORDER BY sequence_namespace.nspname, class_sequence.relname
LOOP
EXECUTE format(
'SELECT COALESCE(MAX(%I), 1), MAX(%I) IS NOT NULL FROM %I.%I',
sequence_record.column_name,
sequence_record.column_name,
sequence_record.table_schema,
sequence_record.table_name
)
INTO max_value, should_be_called;
EXECUTE format(
'SELECT last_value FROM %I.%I',
sequence_record.sequence_schema,
sequence_record.sequence_name
)
INTO prior_value;
EXECUTE format(
'SELECT setval(%L, %s, %L)',
format('%I.%I', sequence_record.sequence_schema, sequence_record.sequence_name),
max_value,
should_be_called
);
sequence_name := format('%I.%I', sequence_record.sequence_schema, sequence_record.sequence_name);
table_name := format('%I.%I', sequence_record.table_schema, sequence_record.table_name);
column_name := sequence_record.column_name;
previous_value := prior_value;
new_value := max_value;
is_called := should_be_called;
RETURN NEXT;
END LOOP;
END;
$$;
`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
DROP FUNCTION IF EXISTS maintenance.reset_owned_sequences(text);
`)
}

View file

@ -97,6 +97,10 @@ import * as migration_20260928_021138_change_ai_model from './20260928_021138_ch
import * as migration_20260930_162200_add_documentation_wiki_category from './20260930_162200_add_documentation_wiki_category'; import * as migration_20260930_162200_add_documentation_wiki_category from './20260930_162200_add_documentation_wiki_category';
import * as migration_20260930_230747_add_tech_categories from './20260930_230747_add_tech_categories'; import * as migration_20260930_230747_add_tech_categories from './20260930_230747_add_tech_categories';
import * as migration_20261001_000320_add_tech_tree_position from './20261001_000320_add_tech_tree_position'; import * as migration_20261001_000320_add_tech_tree_position from './20261001_000320_add_tech_tree_position';
import * as migration_20261001_000400_add_reset_owned_sequences_function from './20261001_000400_add_reset_owned_sequences_function';
import * as migration_20261002_031917_add_promotion_nominations_and_leadership_transfers from './20261002_031917_add_promotion_nominations_and_leadership_transfers';
import * as migration_20261002_180000_add_asset_type_accessory from './20261002_180000_add_asset_type_accessory';
import * as migration_20261003_060154_change_ai_model_to_ornith from './20261003_060154_change_ai_model_to_ornith';
export const migrations = [ export const migrations = [
{ {
@ -592,6 +596,26 @@ export const migrations = [
{ {
up: migration_20261001_000320_add_tech_tree_position.up, up: migration_20261001_000320_add_tech_tree_position.up,
down: migration_20261001_000320_add_tech_tree_position.down, down: migration_20261001_000320_add_tech_tree_position.down,
name: '20261001_000320_add_tech_tree_position' name: '20261001_000320_add_tech_tree_position',
},
{
up: migration_20261001_000400_add_reset_owned_sequences_function.up,
down: migration_20261001_000400_add_reset_owned_sequences_function.down,
name: '20261001_000400_add_reset_owned_sequences_function',
},
{
up: migration_20261002_031917_add_promotion_nominations_and_leadership_transfers.up,
down: migration_20261002_031917_add_promotion_nominations_and_leadership_transfers.down,
name: '20261002_031917_add_promotion_nominations_and_leadership_transfers'
},
{
up: migration_20261002_180000_add_asset_type_accessory.up,
down: migration_20261002_180000_add_asset_type_accessory.down,
name: '20261002_180000_add_asset_type_accessory'
},
{
up: migration_20261003_060154_change_ai_model_to_ornith.up,
down: migration_20261003_060154_change_ai_model_to_ornith.down,
name: '20261003_060154_change_ai_model_to_ornith'
}, },
]; ];

View file

@ -118,6 +118,8 @@ export const enum_game_event_logs_target_collection = pgEnum(
"custom-radio-tests", "custom-radio-tests",
"custom-radio-test-scores", "custom-radio-test-scores",
"ribbon-submissions", "ribbon-submissions",
"promotion-nominations",
"leadership-transfers",
], ],
); );
export const enum_users_roles = pgEnum("enum_users_roles", [ export const enum_users_roles = pgEnum("enum_users_roles", [
@ -575,6 +577,23 @@ export const enum_ribbon_submissions_recipient_scope = pgEnum(
"enum_ribbon_submissions_recipient_scope", "enum_ribbon_submissions_recipient_scope",
["same_command", "whitelist", "anyone"], ["same_command", "whitelist", "anyone"],
); );
export const enum_promotion_nominations_status = pgEnum("enum_promotion_nominations_status", [
"pending",
"awaiting_superuser",
"approved",
"rejected",
"cancelled",
]);
export const enum_promotion_nominations_leader_decision = pgEnum(
"enum_promotion_nominations_leader_decision",
["pending", "endorsed", "rejected", "not_required"],
);
export const enum_leadership_transfers_status = pgEnum("enum_leadership_transfers_status", [
"pending",
"approved",
"denied",
"cancelled",
]);
export const enum_missions_factions_involved_threat_level = pgEnum( export const enum_missions_factions_involved_threat_level = pgEnum(
"enum_missions_factions_involved_threat_level", "enum_missions_factions_involved_threat_level",
["ally", "friendly", "neutral", "hostile"], ["ally", "friendly", "neutral", "hostile"],
@ -1031,7 +1050,7 @@ export const enum_plugin_ai_instructions_field_type = pgEnum(
["text", "textarea", "upload", "richText"], ["text", "textarea", "upload", "richText"],
); );
export const enum_plugin_ai_instructions_model_id = pgEnum("enum_plugin_ai_instructions_model_id", [ export const enum_plugin_ai_instructions_model_id = pgEnum("enum_plugin_ai_instructions_model_id", [
"unsloth/gemma-4-26B-A4B-it-qat-GGUF", "unsloth/Ornith-1.0-9B-GGUF",
]); ]);
export const enum_shims_shims_targeting_roles = pgEnum("enum_shims_shims_targeting_roles", [ export const enum_shims_shims_targeting_roles = pgEnum("enum_shims_shims_targeting_roles", [
"guest", "guest",
@ -2741,6 +2760,104 @@ export const ribbon_submissions = pgTable(
], ],
); );
export const promotion_nominations = pgTable(
"promotion_nominations",
{
id: serial("id").primaryKey(),
nominee: integer("nominee_id")
.notNull()
.references(() => users.id, {
onDelete: "set null",
}),
nominator: integer("nominator_id")
.notNull()
.references(() => users.id, {
onDelete: "set null",
}),
accolades: varchar("accolades").notNull(),
status: enum_promotion_nominations_status("status").notNull().default("pending"),
leaderDecision:
enum_promotion_nominations_leader_decision("leader_decision").default("pending"),
leaderDecisionBy: integer("leader_decision_by_id").references(() => users.id, {
onDelete: "set null",
}),
leaderDecidedAt: timestamp("leader_decided_at", {
mode: "string",
withTimezone: true,
precision: 3,
}),
leaderNote: varchar("leader_note"),
reviewNote: varchar("review_note"),
reviewedBy: integer("reviewed_by_id").references(() => users.id, {
onDelete: "set null",
}),
reviewedAt: timestamp("reviewed_at", { mode: "string", withTimezone: true, precision: 3 }),
updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 })
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", { mode: "string", withTimezone: true, precision: 3 })
.defaultNow()
.notNull(),
},
(columns) => [
index("promotion_nominations_nominee_idx").on(columns.nominee),
index("promotion_nominations_nominator_idx").on(columns.nominator),
index("promotion_nominations_leader_decision_by_idx").on(columns.leaderDecisionBy),
index("promotion_nominations_reviewed_by_idx").on(columns.reviewedBy),
index("promotion_nominations_updated_at_idx").on(columns.updatedAt),
index("promotion_nominations_created_at_idx").on(columns.createdAt),
],
);
export const leadership_transfers = pgTable(
"leadership_transfers",
{
id: serial("id").primaryKey(),
initiator: integer("initiator_id")
.notNull()
.references(() => users.id, {
onDelete: "set null",
}),
subject: integer("subject_id")
.notNull()
.references(() => users.id, {
onDelete: "set null",
}),
fromAssignment: integer("from_assignment_id")
.notNull()
.references(() => assignments.id, {
onDelete: "set null",
}),
toAssignment: integer("to_assignment_id")
.notNull()
.references(() => assignments.id, {
onDelete: "set null",
}),
reason: varchar("reason"),
status: enum_leadership_transfers_status("status").notNull().default("pending"),
reviewNote: varchar("review_note"),
reviewedBy: integer("reviewed_by_id").references(() => users.id, {
onDelete: "set null",
}),
resolvedAt: timestamp("resolved_at", { mode: "string", withTimezone: true, precision: 3 }),
updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 })
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", { mode: "string", withTimezone: true, precision: 3 })
.defaultNow()
.notNull(),
},
(columns) => [
index("leadership_transfers_initiator_idx").on(columns.initiator),
index("leadership_transfers_subject_idx").on(columns.subject),
index("leadership_transfers_from_assignment_idx").on(columns.fromAssignment),
index("leadership_transfers_to_assignment_idx").on(columns.toAssignment),
index("leadership_transfers_reviewed_by_idx").on(columns.reviewedBy),
index("leadership_transfers_updated_at_idx").on(columns.updatedAt),
index("leadership_transfers_created_at_idx").on(columns.createdAt),
],
);
export const missions_mission_roles_provided_roles = pgTable( export const missions_mission_roles_provided_roles = pgTable(
"missions_mission_roles_provided_roles", "missions_mission_roles_provided_roles",
{ {
@ -6752,8 +6869,8 @@ export const plugin_ai_instructions = pgTable(
layout: varchar("layout").default( layout: varchar("layout").default(
'[paragraph] - Write a concise introduction (2-3 sentences) that outlines the main topic.\n[horizontalrule] - Insert a horizontal rule to separate the introduction from the main content.\n[list] - Create a list with 3-5 items. Each list item should contain:\n a. [heading] - A brief, descriptive heading (up to 5 words)\n b. [paragraph] - A short explanation or elaboration (1-2 sentences)\n[horizontalrule] - Insert another horizontal rule to separate the main content from the conclusion.\n[paragraph] - Compose a brief conclusion (2-3 sentences) summarizing the key points.\n[quote] - Include a relevant quote from a famous person, directly related to the topic. Format: "Quote text." - Author Name', '[paragraph] - Write a concise introduction (2-3 sentences) that outlines the main topic.\n[horizontalrule] - Insert a horizontal rule to separate the introduction from the main content.\n[list] - Create a list with 3-5 items. Each list item should contain:\n a. [heading] - A brief, descriptive heading (up to 5 words)\n b. [paragraph] - A short explanation or elaboration (1-2 sentences)\n[horizontalrule] - Insert another horizontal rule to separate the main content from the conclusion.\n[paragraph] - Compose a brief conclusion (2-3 sentences) summarizing the key points.\n[quote] - Include a relevant quote from a famous person, directly related to the topic. Format: "Quote text." - Author Name',
), ),
"gemma-4-settings_model": varchar("gemma_4_settings_model").default( "ornith-9b-settings_model": varchar("ornith_9b_settings_model").default(
"unsloth/gemma-4-26B-A4B-it-qat-GGUF", "unsloth/Ornith-1.0-9B-GGUF",
), ),
updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 }) updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 })
.defaultNow() .defaultNow()
@ -6864,6 +6981,14 @@ export const payload_mcp_api_keys = pgTable(
ribbonSubmissions_create: boolean("ribbon_submissions_create").default(false), ribbonSubmissions_create: boolean("ribbon_submissions_create").default(false),
ribbonSubmissions_update: boolean("ribbon_submissions_update").default(false), ribbonSubmissions_update: boolean("ribbon_submissions_update").default(false),
ribbonSubmissions_delete: boolean("ribbon_submissions_delete").default(false), ribbonSubmissions_delete: boolean("ribbon_submissions_delete").default(false),
promotionNominations_find: boolean("promotion_nominations_find").default(false),
promotionNominations_create: boolean("promotion_nominations_create").default(false),
promotionNominations_update: boolean("promotion_nominations_update").default(false),
promotionNominations_delete: boolean("promotion_nominations_delete").default(false),
leadershipTransfers_find: boolean("leadership_transfers_find").default(false),
leadershipTransfers_create: boolean("leadership_transfers_create").default(false),
leadershipTransfers_update: boolean("leadership_transfers_update").default(false),
leadershipTransfers_delete: boolean("leadership_transfers_delete").default(false),
missions_find: boolean("missions_find").default(false), missions_find: boolean("missions_find").default(false),
missions_create: boolean("missions_create").default(false), missions_create: boolean("missions_create").default(false),
missions_update: boolean("missions_update").default(false), missions_update: boolean("missions_update").default(false),
@ -7158,6 +7283,8 @@ export const payload_locked_documents_rels = pgTable(
"user-notificationsID": integer("user_notifications_id"), "user-notificationsID": integer("user_notifications_id"),
evaluationsID: integer("evaluations_id"), evaluationsID: integer("evaluations_id"),
"ribbon-submissionsID": integer("ribbon_submissions_id"), "ribbon-submissionsID": integer("ribbon_submissions_id"),
"promotion-nominationsID": integer("promotion_nominations_id"),
"leadership-transfersID": integer("leadership_transfers_id"),
missionsID: integer("missions_id"), missionsID: integer("missions_id"),
"mission-attendancesID": integer("mission_attendances_id"), "mission-attendancesID": integer("mission_attendances_id"),
"mission-role-signupsID": integer("mission_role_signups_id"), "mission-role-signupsID": integer("mission_role_signups_id"),
@ -7253,6 +7380,12 @@ export const payload_locked_documents_rels = pgTable(
index("payload_locked_documents_rels_ribbon_submissions_id_idx").on( index("payload_locked_documents_rels_ribbon_submissions_id_idx").on(
columns["ribbon-submissionsID"], columns["ribbon-submissionsID"],
), ),
index("payload_locked_documents_rels_promotion_nominations_id_idx").on(
columns["promotion-nominationsID"],
),
index("payload_locked_documents_rels_leadership_transfers_id_idx").on(
columns["leadership-transfersID"],
),
index("payload_locked_documents_rels_missions_id_idx").on(columns.missionsID), index("payload_locked_documents_rels_missions_id_idx").on(columns.missionsID),
index("payload_locked_documents_rels_mission_attendances_id_idx").on( index("payload_locked_documents_rels_mission_attendances_id_idx").on(
columns["mission-attendancesID"], columns["mission-attendancesID"],
@ -7462,6 +7595,16 @@ export const payload_locked_documents_rels = pgTable(
foreignColumns: [ribbon_submissions.id], foreignColumns: [ribbon_submissions.id],
name: "payload_locked_documents_rels_ribbon_submissions_fk", name: "payload_locked_documents_rels_ribbon_submissions_fk",
}).onDelete("cascade"), }).onDelete("cascade"),
foreignKey({
columns: [columns["promotion-nominationsID"]],
foreignColumns: [promotion_nominations.id],
name: "payload_locked_documents_rels_promotion_nominations_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["leadership-transfersID"]],
foreignColumns: [leadership_transfers.id],
name: "payload_locked_documents_rels_leadership_transfers_fk",
}).onDelete("cascade"),
foreignKey({ foreignKey({
columns: [columns["missionsID"]], columns: [columns["missionsID"]],
foreignColumns: [missions.id], foreignColumns: [missions.id],
@ -7851,6 +7994,9 @@ export const game_rules = pgTable(
currencySingular: varchar("currency_singular"), currencySingular: varchar("currency_singular"),
currencyPlural: varchar("currency_plural"), currencyPlural: varchar("currency_plural"),
currencyDenomination: varchar("currency_denomination"), currencyDenomination: varchar("currency_denomination"),
infantryAssignment: integer("infantry_assignment_id").references(() => assignments.id, {
onDelete: "set null",
}),
proximityThreshold: numeric("proximity_threshold", { mode: "number" }).default(1000), proximityThreshold: numeric("proximity_threshold", { mode: "number" }).default(1000),
gameTickIntervalMinutes: numeric("game_tick_interval_minutes", { mode: "number" }).default(10), gameTickIntervalMinutes: numeric("game_tick_interval_minutes", { mode: "number" }).default(10),
lockerGridWidth: numeric("locker_grid_width", { mode: "number" }).default(10), lockerGridWidth: numeric("locker_grid_width", { mode: "number" }).default(10),
@ -7883,6 +8029,7 @@ export const game_rules = pgTable(
(columns) => [ (columns) => [
index("game_rules_main_currency_idx").on(columns.mainCurrency), index("game_rules_main_currency_idx").on(columns.mainCurrency),
index("game_rules_currency_symbol_idx").on(columns.currencySymbol), index("game_rules_currency_symbol_idx").on(columns.currencySymbol),
index("game_rules_infantry_assignment_idx").on(columns.infantryAssignment),
], ],
); );
@ -8640,6 +8787,55 @@ export const relations_ribbon_submissions = relations(ribbon_submissions, ({ one
relationName: "grantorTypes", relationName: "grantorTypes",
}), }),
})); }));
export const relations_promotion_nominations = relations(promotion_nominations, ({ one }) => ({
nominee: one(users, {
fields: [promotion_nominations.nominee],
references: [users.id],
relationName: "nominee",
}),
nominator: one(users, {
fields: [promotion_nominations.nominator],
references: [users.id],
relationName: "nominator",
}),
leaderDecisionBy: one(users, {
fields: [promotion_nominations.leaderDecisionBy],
references: [users.id],
relationName: "leaderDecisionBy",
}),
reviewedBy: one(users, {
fields: [promotion_nominations.reviewedBy],
references: [users.id],
relationName: "reviewedBy",
}),
}));
export const relations_leadership_transfers = relations(leadership_transfers, ({ one }) => ({
initiator: one(users, {
fields: [leadership_transfers.initiator],
references: [users.id],
relationName: "initiator",
}),
subject: one(users, {
fields: [leadership_transfers.subject],
references: [users.id],
relationName: "subject",
}),
fromAssignment: one(assignments, {
fields: [leadership_transfers.fromAssignment],
references: [assignments.id],
relationName: "fromAssignment",
}),
toAssignment: one(assignments, {
fields: [leadership_transfers.toAssignment],
references: [assignments.id],
relationName: "toAssignment",
}),
reviewedBy: one(users, {
fields: [leadership_transfers.reviewedBy],
references: [users.id],
relationName: "reviewedBy",
}),
}));
export const relations_missions_mission_roles_provided_roles = relations( export const relations_missions_mission_roles_provided_roles = relations(
missions_mission_roles_provided_roles, missions_mission_roles_provided_roles,
({ one }) => ({ ({ one }) => ({
@ -10535,6 +10731,16 @@ export const relations_payload_locked_documents_rels = relations(
references: [ribbon_submissions.id], references: [ribbon_submissions.id],
relationName: "ribbon-submissions", relationName: "ribbon-submissions",
}), }),
"promotion-nominationsID": one(promotion_nominations, {
fields: [payload_locked_documents_rels["promotion-nominationsID"]],
references: [promotion_nominations.id],
relationName: "promotion-nominations",
}),
"leadership-transfersID": one(leadership_transfers, {
fields: [payload_locked_documents_rels["leadership-transfersID"]],
references: [leadership_transfers.id],
relationName: "leadership-transfers",
}),
missionsID: one(missions, { missionsID: one(missions, {
fields: [payload_locked_documents_rels.missionsID], fields: [payload_locked_documents_rels.missionsID],
references: [missions.id], references: [missions.id],
@ -10889,6 +11095,11 @@ export const relations_game_rules = relations(game_rules, ({ one, many }) => ({
references: [media.id], references: [media.id],
relationName: "currencySymbol", relationName: "currencySymbol",
}), }),
infantryAssignment: one(assignments, {
fields: [game_rules.infantryAssignment],
references: [assignments.id],
relationName: "infantryAssignment",
}),
_rels: many(game_rules_rels, { _rels: many(game_rules_rels, {
relationName: "_rels", relationName: "_rels",
}), }),
@ -10974,6 +11185,9 @@ type DatabaseSchema = {
enum_ribbon_submissions_grantor_types: typeof enum_ribbon_submissions_grantor_types; enum_ribbon_submissions_grantor_types: typeof enum_ribbon_submissions_grantor_types;
enum_ribbon_submissions_status: typeof enum_ribbon_submissions_status; enum_ribbon_submissions_status: typeof enum_ribbon_submissions_status;
enum_ribbon_submissions_recipient_scope: typeof enum_ribbon_submissions_recipient_scope; enum_ribbon_submissions_recipient_scope: typeof enum_ribbon_submissions_recipient_scope;
enum_promotion_nominations_status: typeof enum_promotion_nominations_status;
enum_promotion_nominations_leader_decision: typeof enum_promotion_nominations_leader_decision;
enum_leadership_transfers_status: typeof enum_leadership_transfers_status;
enum_missions_factions_involved_threat_level: typeof enum_missions_factions_involved_threat_level; enum_missions_factions_involved_threat_level: typeof enum_missions_factions_involved_threat_level;
enum_missions_logistics_and_support_insertion: typeof enum_missions_logistics_and_support_insertion; enum_missions_logistics_and_support_insertion: typeof enum_missions_logistics_and_support_insertion;
enum_missions_logistics_and_support_extraction: typeof enum_missions_logistics_and_support_extraction; enum_missions_logistics_and_support_extraction: typeof enum_missions_logistics_and_support_extraction;
@ -11109,6 +11323,8 @@ type DatabaseSchema = {
evaluations: typeof evaluations; evaluations: typeof evaluations;
ribbon_submissions_grantor_types: typeof ribbon_submissions_grantor_types; ribbon_submissions_grantor_types: typeof ribbon_submissions_grantor_types;
ribbon_submissions: typeof ribbon_submissions; ribbon_submissions: typeof ribbon_submissions;
promotion_nominations: typeof promotion_nominations;
leadership_transfers: typeof leadership_transfers;
missions_mission_roles_provided_roles: typeof missions_mission_roles_provided_roles; missions_mission_roles_provided_roles: typeof missions_mission_roles_provided_roles;
missions_briefing: typeof missions_briefing; missions_briefing: typeof missions_briefing;
missions_mission_objectives: typeof missions_mission_objectives; missions_mission_objectives: typeof missions_mission_objectives;
@ -11299,6 +11515,8 @@ type DatabaseSchema = {
relations_evaluations: typeof relations_evaluations; relations_evaluations: typeof relations_evaluations;
relations_ribbon_submissions_grantor_types: typeof relations_ribbon_submissions_grantor_types; relations_ribbon_submissions_grantor_types: typeof relations_ribbon_submissions_grantor_types;
relations_ribbon_submissions: typeof relations_ribbon_submissions; relations_ribbon_submissions: typeof relations_ribbon_submissions;
relations_promotion_nominations: typeof relations_promotion_nominations;
relations_leadership_transfers: typeof relations_leadership_transfers;
relations_missions_mission_roles_provided_roles: typeof relations_missions_mission_roles_provided_roles; relations_missions_mission_roles_provided_roles: typeof relations_missions_mission_roles_provided_roles;
relations_missions_briefing: typeof relations_missions_briefing; relations_missions_briefing: typeof relations_missions_briefing;
relations_missions_mission_objectives: typeof relations_missions_mission_objectives; relations_missions_mission_objectives: typeof relations_missions_mission_objectives;

View file

@ -89,6 +89,8 @@ export interface Config {
'user-notifications': UserNotification; 'user-notifications': UserNotification;
evaluations: Evaluation; evaluations: Evaluation;
'ribbon-submissions': RibbonSubmission; 'ribbon-submissions': RibbonSubmission;
'promotion-nominations': PromotionNomination;
'leadership-transfers': LeadershipTransfer;
missions: Mission; missions: Mission;
'mission-attendances': MissionAttendance; 'mission-attendances': MissionAttendance;
'mission-role-signups': MissionRoleSignup; 'mission-role-signups': MissionRoleSignup;
@ -183,6 +185,8 @@ export interface Config {
'user-notifications': UserNotificationsSelect<false> | UserNotificationsSelect<true>; 'user-notifications': UserNotificationsSelect<false> | UserNotificationsSelect<true>;
evaluations: EvaluationsSelect<false> | EvaluationsSelect<true>; evaluations: EvaluationsSelect<false> | EvaluationsSelect<true>;
'ribbon-submissions': RibbonSubmissionsSelect<false> | RibbonSubmissionsSelect<true>; 'ribbon-submissions': RibbonSubmissionsSelect<false> | RibbonSubmissionsSelect<true>;
'promotion-nominations': PromotionNominationsSelect<false> | PromotionNominationsSelect<true>;
'leadership-transfers': LeadershipTransfersSelect<false> | LeadershipTransfersSelect<true>;
missions: MissionsSelect<false> | MissionsSelect<true>; missions: MissionsSelect<false> | MissionsSelect<true>;
'mission-attendances': MissionAttendancesSelect<false> | MissionAttendancesSelect<true>; 'mission-attendances': MissionAttendancesSelect<false> | MissionAttendancesSelect<true>;
'mission-role-signups': MissionRoleSignupsSelect<false> | MissionRoleSignupsSelect<true>; 'mission-role-signups': MissionRoleSignupsSelect<false> | MissionRoleSignupsSelect<true>;
@ -1685,6 +1689,7 @@ export interface Asset {
| 'optic' | 'optic'
| 'muzzle' | 'muzzle'
| 'bipod' | 'bipod'
| 'accessory'
| 'uniform' | 'uniform'
| 'vest' | 'vest'
| 'backpack' | 'backpack'
@ -2386,6 +2391,8 @@ export interface GameEventLog {
| 'custom-radio-tests' | 'custom-radio-tests'
| 'custom-radio-test-scores' | 'custom-radio-test-scores'
| 'ribbon-submissions' | 'ribbon-submissions'
| 'promotion-nominations'
| 'leadership-transfers'
) )
| null; | null;
targetId?: number | null; targetId?: number | null;
@ -2951,11 +2958,11 @@ export interface Mission {
*/ */
evaluationRemindersSentAt?: string | null; evaluationRemindersSentAt?: string | null;
/** /**
* Server-local day the last web 'not Ready' reminder notification was sent to the op creator (managed by the mission tick, once per day). * Server-local day the last web 'not Scheduled' reminder notification was sent to the op creator (managed by the mission tick, once per day).
*/ */
readyReminderWebSentAt?: string | null; readyReminderWebSentAt?: string | null;
/** /**
* Server-local day the last 'not Ready' reminder DM was sent to the op creator (managed by the Discord bot, once per day). * Server-local day the last 'not Scheduled' reminder DM was sent to the op creator (managed by the Discord bot, once per day).
*/ */
readyReminderDmSentAt?: string | null; readyReminderDmSentAt?: string | null;
/** /**
@ -2988,6 +2995,9 @@ export interface Mission {
ownershipAndStatus: { ownershipAndStatus: {
authors: (number | User)[]; authors: (number | User)[];
zeus?: (number | User)[] | null; zeus?: (number | User)[] | null;
/**
* Side operations start as Ready (a quiet draft) and are committed to Scheduled by their creator, which posts the Discord roll-call. Only Scheduled/Active missions are driven by the automatic start/complete lifecycle; Concept/Planning/Ready are drafts.
*/
status: 'Concept' | 'Planning' | 'Ready' | 'Scheduled' | 'Active' | 'Completed' | 'Cancelled'; status: 'Concept' | 'Planning' | 'Ready' | 'Scheduled' | 'Active' | 'Completed' | 'Cancelled';
visibility: 'unit' | 'leadership' | 'intel' | 'private'; visibility: 'unit' | 'leadership' | 'intel' | 'private';
/** /**
@ -3270,6 +3280,80 @@ export interface RibbonSubmission {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "promotion-nominations".
*/
export interface PromotionNomination {
id: number;
/**
* Member being nominated for promotion.
*/
nominee: number | User;
/**
* Member submitting the nomination (self for self-requests).
*/
nominator: number | User;
/**
* Written examples of performance, accolades, and reasons this member deserves promotion.
*/
accolades: string;
status: 'pending' | 'awaiting_superuser' | 'approved' | 'rejected' | 'cancelled';
/**
* Decision of the nominee's division leader.
*/
leaderDecision?: ('pending' | 'endorsed' | 'rejected' | 'not_required') | null;
leaderDecisionBy?: (number | null) | User;
leaderDecidedAt?: string | null;
/**
* Note from the division leader's endorsement or rejection.
*/
leaderNote?: string | null;
/**
* Final reviewer feedback shown to the nominator.
*/
reviewNote?: string | null;
reviewedBy?: (number | null) | User;
reviewedAt?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "leadership-transfers".
*/
export interface LeadershipTransfer {
id: number;
/**
* Division leader requesting the removal.
*/
initiator: number | User;
/**
* Member being transferred.
*/
subject: number | User;
/**
* Division the member is removed from.
*/
fromAssignment: number | Assignment;
/**
* Division the member is transferred to (the Infantry assignment).
*/
toAssignment: number | Assignment;
/**
* Optional reason for the removal, visible to command.
*/
reason?: string | null;
status: 'pending' | 'approved' | 'denied' | 'cancelled';
/**
* Reviewer note recorded with the decision.
*/
reviewNote?: string | null;
reviewedBy?: (number | null) | User;
resolvedAt?: string | null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "mission-attendances". * via the `definition` "mission-attendances".
@ -5239,7 +5323,7 @@ export interface PluginAiInstruction {
*/ */
'field-type'?: ('text' | 'textarea' | 'upload' | 'richText') | null; 'field-type'?: ('text' | 'textarea' | 'upload' | 'richText') | null;
'relation-to'?: string | null; 'relation-to'?: string | null;
'model-id'?: 'unsloth/gemma-4-26B-A4B-it-qat-GGUF' | null; 'model-id'?: 'unsloth/Ornith-1.0-9B-GGUF' | null;
/** /**
* Please reload your collection after applying the changes * Please reload your collection after applying the changes
*/ */
@ -5259,7 +5343,7 @@ export interface PluginAiInstruction {
| null; | null;
system?: string | null; system?: string | null;
layout?: string | null; layout?: string | null;
'gemma-4-settings'?: { 'ornith-9b-settings'?: {
model?: string | null; model?: string | null;
}; };
updatedAt: string; updatedAt: string;
@ -5663,6 +5747,42 @@ export interface PayloadMcpApiKey {
*/ */
delete?: boolean | null; delete?: boolean | null;
}; };
promotionNominations?: {
/**
* Allow clients to find promotion-nominations.
*/
find?: boolean | null;
/**
* Allow clients to create promotion-nominations.
*/
create?: boolean | null;
/**
* Allow clients to update promotion-nominations.
*/
update?: boolean | null;
/**
* Allow clients to delete promotion-nominations.
*/
delete?: boolean | null;
};
leadershipTransfers?: {
/**
* Allow clients to find leadership-transfers.
*/
find?: boolean | null;
/**
* Allow clients to create leadership-transfers.
*/
create?: boolean | null;
/**
* Allow clients to update leadership-transfers.
*/
update?: boolean | null;
/**
* Allow clients to delete leadership-transfers.
*/
delete?: boolean | null;
};
missions?: { missions?: {
/** /**
* Allow clients to find missions. * Allow clients to find missions.
@ -6768,6 +6888,14 @@ export interface PayloadLockedDocument {
relationTo: 'ribbon-submissions'; relationTo: 'ribbon-submissions';
value: number | RibbonSubmission; value: number | RibbonSubmission;
} | null) } | null)
| ({
relationTo: 'promotion-nominations';
value: number | PromotionNomination;
} | null)
| ({
relationTo: 'leadership-transfers';
value: number | LeadershipTransfer;
} | null)
| ({ | ({
relationTo: 'missions'; relationTo: 'missions';
value: number | Mission; value: number | Mission;
@ -7696,6 +7824,42 @@ export interface RibbonSubmissionsSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "promotion-nominations_select".
*/
export interface PromotionNominationsSelect<T extends boolean = true> {
nominee?: T;
nominator?: T;
accolades?: T;
status?: T;
leaderDecision?: T;
leaderDecisionBy?: T;
leaderDecidedAt?: T;
leaderNote?: T;
reviewNote?: T;
reviewedBy?: T;
reviewedAt?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "leadership-transfers_select".
*/
export interface LeadershipTransfersSelect<T extends boolean = true> {
initiator?: T;
subject?: T;
fromAssignment?: T;
toAssignment?: T;
reason?: T;
status?: T;
reviewNote?: T;
reviewedBy?: T;
resolvedAt?: T;
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "missions_select". * via the `definition` "missions_select".
@ -9360,7 +9524,7 @@ export interface PluginAiInstructionsSelect<T extends boolean = true> {
}; };
system?: T; system?: T;
layout?: T; layout?: T;
'gemma-4-settings'?: 'ornith-9b-settings'?:
| T | T
| { | {
model?: T; model?: T;
@ -9544,6 +9708,22 @@ export interface PayloadMcpApiKeysSelect<T extends boolean = true> {
update?: T; update?: T;
delete?: T; delete?: T;
}; };
promotionNominations?:
| T
| {
find?: T;
create?: T;
update?: T;
delete?: T;
};
leadershipTransfers?:
| T
| {
find?: T;
create?: T;
update?: T;
delete?: T;
};
missions?: missions?:
| T | T
| { | {
@ -10068,6 +10248,10 @@ export interface GameRule {
* Assignment(s) considered the intelligence division. Members and leaders of these assignments get elevated access to draft missions and intelligence content. * Assignment(s) considered the intelligence division. Members and leaders of these assignments get elevated access to draft missions and intelligence content.
*/ */
intelAssignments?: (number | Assignment)[] | null; intelAssignments?: (number | Assignment)[] | null;
/**
* Assignment that members are moved to when a division leader removes them from their division (the default destination for leadership-initiated transfers).
*/
infantryAssignment?: (number | null) | Assignment;
/** /**
* Distance threshold in real meters for instant transfers. Structures within this distance can transfer resources instantly. Beyond this, shipments are required. * Distance threshold in real meters for instant transfers. Structures within this distance can transfer resources instantly. Beyond this, shipments are required.
*/ */
@ -10212,6 +10396,7 @@ export interface GameRulesSelect<T extends boolean = true> {
currencyDenomination?: T; currencyDenomination?: T;
commandAssignments?: T; commandAssignments?: T;
intelAssignments?: T; intelAssignments?: T;
infantryAssignment?: T;
proximityThreshold?: T; proximityThreshold?: T;
gameTickIntervalMinutes?: T; gameTickIntervalMinutes?: T;
lockerGridWidth?: T; lockerGridWidth?: T;