volleyballsimtypes 0.0.459 → 0.0.462

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.
@@ -16,6 +16,7 @@ function initModels(sequelize) {
16
16
  const UserPullGrant = models_1.UserPullGrantModel.initModel(sequelize);
17
17
  const Wishlist = models_1.WishlistModel.initModel(sequelize);
18
18
  const UserSettings = models_1.UserSettingsModel.initModel(sequelize);
19
+ const UserDeviceToken = models_1.UserDeviceTokenModel.initModel(sequelize);
19
20
  const Notification = models_1.NotificationModel.initModel(sequelize);
20
21
  const AuthIdentity = models_1.AuthIdentityModel.initModel(sequelize);
21
22
  const AuthSession = models_1.AuthSessionModel.initModel(sequelize);
@@ -63,7 +64,9 @@ function initModels(sequelize) {
63
64
  AuthUser.hasOne(GachaPity, { as: 'GachaPity', foreignKey: 'user_id' });
64
65
  AuthUser.hasOne(Wishlist, { as: 'Wishlist', foreignKey: 'user_id' });
65
66
  AuthUser.hasOne(UserSettings, { as: 'UserSettings', foreignKey: 'user_id' });
67
+ AuthUser.hasMany(UserDeviceToken, { as: 'UserDeviceTokens', foreignKey: 'user_id' });
66
68
  AuthUser.hasMany(Notification, { as: 'Notifications', foreignKey: 'user_id' });
69
+ UserDeviceToken.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
67
70
  Notification.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
68
71
  AuthUser.hasMany(GachaPullHistory, { as: 'GachaPullHistory', foreignKey: 'user_id' });
69
72
  AuthUser.hasMany(UserPullGrant, { as: 'UserPullGrants', foreignKey: 'user_id' });
@@ -294,6 +297,7 @@ function initModels(sequelize) {
294
297
  UserPullGrant,
295
298
  Wishlist,
296
299
  UserSettings,
300
+ UserDeviceToken,
297
301
  Notification,
298
302
  Coach,
299
303
  Scout,
@@ -44,6 +44,7 @@ export * from './wishlist';
44
44
  export * from './currency-transaction';
45
45
  export * from './user-pull-grant';
46
46
  export * from './user-settings';
47
+ export * from './user-device-token';
47
48
  export * from './notification';
48
49
  export * from './vper';
49
50
  export * from './views';
@@ -60,6 +60,7 @@ __exportStar(require("./wishlist"), exports);
60
60
  __exportStar(require("./currency-transaction"), exports);
61
61
  __exportStar(require("./user-pull-grant"), exports);
62
62
  __exportStar(require("./user-settings"), exports);
63
+ __exportStar(require("./user-device-token"), exports);
63
64
  __exportStar(require("./notification"), exports);
64
65
  __exportStar(require("./vper"), exports);
65
66
  __exportStar(require("./views"), exports);
@@ -0,0 +1,23 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ export interface UserDeviceTokenAttributes {
4
+ user_device_token_id: string;
5
+ user_id: string;
6
+ device_token: string;
7
+ platform: string;
8
+ created_at?: Date;
9
+ updated_at?: Date;
10
+ }
11
+ export type UserDeviceTokenPk = 'user_device_token_id';
12
+ export type UserDeviceTokenId = UserDeviceTokenModel[UserDeviceTokenPk];
13
+ export type UserDeviceTokenOptionalAttributes = 'user_device_token_id' | 'platform' | 'created_at' | 'updated_at';
14
+ export type UserDeviceTokenCreationAttributes = Optional<UserDeviceTokenAttributes, UserDeviceTokenOptionalAttributes>;
15
+ export declare class UserDeviceTokenModel extends Model<UserDeviceTokenAttributes, UserDeviceTokenCreationAttributes> implements UserDeviceTokenAttributes {
16
+ user_device_token_id: string;
17
+ user_id: string;
18
+ device_token: string;
19
+ platform: string;
20
+ created_at?: Date;
21
+ updated_at?: Date;
22
+ static initModel(sequelize: Sequelize.Sequelize): typeof UserDeviceTokenModel;
23
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UserDeviceTokenModel = void 0;
4
+ const sequelize_1 = require("sequelize");
5
+ class UserDeviceTokenModel extends sequelize_1.Model {
6
+ static initModel(sequelize) {
7
+ return UserDeviceTokenModel.init({
8
+ user_device_token_id: {
9
+ type: sequelize_1.DataTypes.UUID,
10
+ allowNull: false,
11
+ primaryKey: true,
12
+ defaultValue: sequelize_1.DataTypes.UUIDV4
13
+ },
14
+ user_id: {
15
+ type: sequelize_1.DataTypes.UUID,
16
+ allowNull: false,
17
+ references: {
18
+ model: 'AuthUser',
19
+ key: 'user_id'
20
+ }
21
+ },
22
+ device_token: {
23
+ type: sequelize_1.DataTypes.TEXT,
24
+ allowNull: false
25
+ },
26
+ platform: {
27
+ type: sequelize_1.DataTypes.TEXT,
28
+ allowNull: false,
29
+ defaultValue: 'android'
30
+ },
31
+ created_at: {
32
+ type: sequelize_1.DataTypes.DATE,
33
+ allowNull: false,
34
+ defaultValue: sequelize_1.DataTypes.NOW
35
+ },
36
+ updated_at: {
37
+ type: sequelize_1.DataTypes.DATE,
38
+ allowNull: false,
39
+ defaultValue: sequelize_1.DataTypes.NOW
40
+ }
41
+ }, {
42
+ sequelize,
43
+ tableName: 'UserDeviceToken',
44
+ schema: 'public',
45
+ timestamps: false,
46
+ indexes: [
47
+ {
48
+ name: 'UserDeviceToken_pk',
49
+ unique: true,
50
+ fields: [{ name: 'user_device_token_id' }]
51
+ },
52
+ {
53
+ name: 'UserDeviceToken_user_id_device_token_uq',
54
+ unique: true,
55
+ fields: [{ name: 'user_id' }, { name: 'device_token' }]
56
+ },
57
+ {
58
+ name: 'UserDeviceToken_user_id_idx',
59
+ fields: [{ name: 'user_id' }]
60
+ }
61
+ ]
62
+ });
63
+ }
64
+ }
65
+ exports.UserDeviceTokenModel = UserDeviceTokenModel;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const globals_1 = require("@jest/globals");
4
4
  const tactics_1 = require("./tactics");
5
5
  const service_1 = require("../../service");
6
+ const test_helpers_1 = require("../../service/test-helpers");
6
7
  // Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
7
8
  function makeTactics(designatedSubs) {
8
9
  return {
@@ -77,4 +78,64 @@ function makeTactics(designatedSubs) {
77
78
  (0, globals_1.expect)(dto.secondLibero).toBe('reserve1');
78
79
  (0, globals_1.expect)(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
79
80
  });
81
+ (0, globals_1.it)('passes per-set tactics columns through to the API DTO shape', () => {
82
+ const model = makeTactics([]);
83
+ model.system_sets = [{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
84
+ model.offensive_preference_sets = [[{ rotation: 1, order: ['rf', 'mf'] }]];
85
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
86
+ (0, globals_1.expect)(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
87
+ (0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, order: ['rf', 'mf'] }]]);
88
+ });
89
+ (0, globals_1.it)('leaves the per-set DTO fields undefined when the columns are absent', () => {
90
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
91
+ (0, globals_1.expect)(dto.systemSets).toBeUndefined();
92
+ (0, globals_1.expect)(dto.offensivePreferenceSets).toBeUndefined();
93
+ });
94
+ });
95
+ (0, globals_1.describe)('per-set tactics — service <-> row round-trip', () => {
96
+ const country = (0, test_helpers_1.makeCountry)();
97
+ const p1 = (0, test_helpers_1.makePlayer)(country);
98
+ const p2 = (0, test_helpers_1.makePlayer)(country);
99
+ const p3 = (0, test_helpers_1.makePlayer)(country);
100
+ const p4 = (0, test_helpers_1.makePlayer)(country);
101
+ const p5 = (0, test_helpers_1.makePlayer)(country);
102
+ const p6 = (0, test_helpers_1.makePlayer)(country);
103
+ const roster = [p1, p2, p3, p4, p5, p6];
104
+ const tactics = service_1.Tactics.create({
105
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
106
+ liberoReplacements: [],
107
+ systemSets: [
108
+ { rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
109
+ { rotationSystem: service_1.RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
110
+ ],
111
+ offensivePreferenceSets: [[{ rotation: 1, order: [p2, p3] }]]
112
+ });
113
+ (0, globals_1.it)('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
114
+ const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
115
+ (0, globals_1.expect)(attrs.system_sets).toEqual([
116
+ { rotationSystem: '5-1', designatedSetters: [p1.id] },
117
+ { rotationSystem: '6-0', designatedSetters: [] }
118
+ ]);
119
+ (0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, order: [p2.id, p3.id] }]]);
120
+ });
121
+ (0, globals_1.it)('transformToTactics resolves the stored ids back to roster players', () => {
122
+ const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
123
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
124
+ (0, globals_1.expect)(back.systemSets?.[0].rotationSystem).toBe(service_1.RotationSystemEnum.FIVE_ONE);
125
+ (0, globals_1.expect)(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
126
+ (0, globals_1.expect)(back.systemSets?.[1].designatedSetters).toEqual([]);
127
+ (0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].order.map(p => p.id)).toEqual([p2.id, p3.id]);
128
+ });
129
+ (0, globals_1.it)('omits the per-set columns entirely for an all-sets config', () => {
130
+ const flat = service_1.Tactics.create({
131
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
132
+ liberoReplacements: []
133
+ });
134
+ const attrs = (0, tactics_1.transformFromTactics)(flat, 'team-1');
135
+ (0, globals_1.expect)(attrs.system_sets).toBeUndefined();
136
+ (0, globals_1.expect)(attrs.offensive_preference_sets).toBeUndefined();
137
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
138
+ (0, globals_1.expect)(back.systemSets).toBeUndefined();
139
+ (0, globals_1.expect)(back.offensivePreferenceSets).toBeUndefined();
140
+ });
80
141
  });
@@ -69,3 +69,118 @@ const test_helpers_1 = require("../../test-helpers");
69
69
  }
70
70
  });
71
71
  });
72
+ (0, globals_1.describe)('TacticsInputSchema — per-set tactics (systemSets / offensivePreferenceSets)', () => {
73
+ const country = (0, test_helpers_1.makeCountry)();
74
+ const p1 = (0, test_helpers_1.makePlayer)(country);
75
+ const p2 = (0, test_helpers_1.makePlayer)(country);
76
+ const p3 = (0, test_helpers_1.makePlayer)(country);
77
+ const p4 = (0, test_helpers_1.makePlayer)(country);
78
+ const p5 = (0, test_helpers_1.makePlayer)(country);
79
+ const p6 = (0, test_helpers_1.makePlayer)(country);
80
+ const reserve = (0, test_helpers_1.makePlayer)(country);
81
+ // Minimal valid flat input (6-0 default: no setters, no preferences). Zone layout: p1@1, p2@2, p3@3,
82
+ // p4@4, p5@5, p6@6 — opposite slot pairs (3 apart) are (1,4), (2,5), (3,6).
83
+ function baseInput(overrides = {}) {
84
+ return {
85
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
86
+ liberoReplacements: [],
87
+ ...overrides
88
+ };
89
+ }
90
+ (0, globals_1.it)('accepts valid per-set overrides for both scopes', () => {
91
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
92
+ systemSets: [
93
+ { rotationSystem: '5-1', designatedSetters: [p1] },
94
+ { rotationSystem: '6-0', designatedSetters: [] },
95
+ { rotationSystem: '4-2', designatedSetters: [p1, p4] }
96
+ ],
97
+ offensivePreferenceSets: [
98
+ [{ rotation: 1, order: [p2, p3] }],
99
+ []
100
+ ]
101
+ }));
102
+ (0, globals_1.expect)(res.success).toBe(true);
103
+ });
104
+ (0, globals_1.it)('carries the per-set fields through Tactics.create parsing', () => {
105
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
106
+ systemSets: [{ rotationSystem: '5-1', designatedSetters: [p1] }],
107
+ offensivePreferenceSets: [[{ rotation: 2, order: [p3] }]]
108
+ }));
109
+ (0, globals_1.expect)(res.success).toBe(true);
110
+ if (res.success) {
111
+ (0, globals_1.expect)(res.data.systemSets?.[0].rotationSystem).toBe('5-1');
112
+ (0, globals_1.expect)(res.data.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
113
+ (0, globals_1.expect)(res.data.offensivePreferenceSets?.[0][0]).toMatchObject({ rotation: 2 });
114
+ }
115
+ });
116
+ (0, globals_1.it)('rejects more than 5 per-set entries', () => {
117
+ const entry = { rotationSystem: '6-0', designatedSetters: [] };
118
+ (0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ systemSets: Array.from({ length: 6 }, () => entry) })).success).toBe(false);
119
+ (0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ offensivePreferenceSets: Array.from({ length: 6 }, () => []) })).success).toBe(false);
120
+ });
121
+ (0, globals_1.it)('rejects a set entry whose setter count does not match its system (5-1 with none)', () => {
122
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
123
+ systemSets: [{ rotationSystem: '5-1', designatedSetters: [] }]
124
+ }));
125
+ (0, globals_1.expect)(res.success).toBe(false);
126
+ if (!res.success) {
127
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTERS_COUNT_5-1_EXPECTED_1');
128
+ (0, globals_1.expect)(issue?.path).toEqual(['systemSets', 0]);
129
+ }
130
+ });
131
+ (0, globals_1.it)('rejects a set entry whose setter is not a court starter', () => {
132
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
133
+ systemSets: [
134
+ { rotationSystem: '6-0', designatedSetters: [] },
135
+ { rotationSystem: '5-1', designatedSetters: [reserve] }
136
+ ]
137
+ }));
138
+ (0, globals_1.expect)(res.success).toBe(false);
139
+ if (!res.success) {
140
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTER_NOT_A_STARTER');
141
+ (0, globals_1.expect)(issue?.path).toEqual(['systemSets', 1]);
142
+ }
143
+ });
144
+ (0, globals_1.it)('rejects a 4-2 set entry whose setters are not in opposite slots', () => {
145
+ // p1@1 and p2@2 are adjacent (not 3 apart), so the 4-2 pair rule fails for that set entry.
146
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
147
+ systemSets: [{ rotationSystem: '4-2', designatedSetters: [p1, p2] }]
148
+ }));
149
+ (0, globals_1.expect)(res.success).toBe(false);
150
+ if (!res.success) {
151
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS')).toBe(true);
152
+ }
153
+ });
154
+ (0, globals_1.it)('rejects a per-set preferences list with a duplicate rotation', () => {
155
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
156
+ offensivePreferenceSets: [[{ rotation: 1, order: [p2] }, { rotation: 1, order: [p3] }]]
157
+ }));
158
+ (0, globals_1.expect)(res.success).toBe(false);
159
+ if (!res.success) {
160
+ const issue = res.error.issues.find(i => i.message === 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION');
161
+ (0, globals_1.expect)(issue?.path).toEqual(['offensivePreferenceSets', 0]);
162
+ }
163
+ });
164
+ (0, globals_1.it)('rejects a per-set preferences order that lists a non-starter', () => {
165
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
166
+ offensivePreferenceSets: [[{ rotation: 1, order: [reserve] }]]
167
+ }));
168
+ (0, globals_1.expect)(res.success).toBe(false);
169
+ if (!res.success) {
170
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
171
+ }
172
+ });
173
+ (0, globals_1.it)('still validates the flat fields when per-set overrides are present', () => {
174
+ // Flat 5-1 with no setter is invalid even though every set entry is valid.
175
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
176
+ rotationSystem: '5-1',
177
+ designatedSetters: [],
178
+ systemSets: [{ rotationSystem: '6-0', designatedSetters: [] }]
179
+ }));
180
+ (0, globals_1.expect)(res.success).toBe(false);
181
+ if (!res.success) {
182
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTERS_COUNT_5-1_EXPECTED_1');
183
+ (0, globals_1.expect)(issue?.path).toEqual(['designatedSetters']);
184
+ }
185
+ });
186
+ });
@@ -1,4 +1,4 @@
1
- import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, ScoutModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerDeclineLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, FriendlyModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, LineupPresetModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel, AppConfigModel, LegacyTeamFlagModel } from './models';
1
+ import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, ScoutModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, UserDeviceTokenModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerDeclineLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, FriendlyModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, LineupPresetModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel, AppConfigModel, LegacyTeamFlagModel } from './models';
2
2
  export function initModels(sequelize) {
3
3
  const Coach = CoachModel.initModel(sequelize);
4
4
  const Scout = ScoutModel.initModel(sequelize);
@@ -13,6 +13,7 @@ export function initModels(sequelize) {
13
13
  const UserPullGrant = UserPullGrantModel.initModel(sequelize);
14
14
  const Wishlist = WishlistModel.initModel(sequelize);
15
15
  const UserSettings = UserSettingsModel.initModel(sequelize);
16
+ const UserDeviceToken = UserDeviceTokenModel.initModel(sequelize);
16
17
  const Notification = NotificationModel.initModel(sequelize);
17
18
  const AuthIdentity = AuthIdentityModel.initModel(sequelize);
18
19
  const AuthSession = AuthSessionModel.initModel(sequelize);
@@ -60,7 +61,9 @@ export function initModels(sequelize) {
60
61
  AuthUser.hasOne(GachaPity, { as: 'GachaPity', foreignKey: 'user_id' });
61
62
  AuthUser.hasOne(Wishlist, { as: 'Wishlist', foreignKey: 'user_id' });
62
63
  AuthUser.hasOne(UserSettings, { as: 'UserSettings', foreignKey: 'user_id' });
64
+ AuthUser.hasMany(UserDeviceToken, { as: 'UserDeviceTokens', foreignKey: 'user_id' });
63
65
  AuthUser.hasMany(Notification, { as: 'Notifications', foreignKey: 'user_id' });
66
+ UserDeviceToken.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
64
67
  Notification.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
65
68
  AuthUser.hasMany(GachaPullHistory, { as: 'GachaPullHistory', foreignKey: 'user_id' });
66
69
  AuthUser.hasMany(UserPullGrant, { as: 'UserPullGrants', foreignKey: 'user_id' });
@@ -291,6 +294,7 @@ export function initModels(sequelize) {
291
294
  UserPullGrant,
292
295
  Wishlist,
293
296
  UserSettings,
297
+ UserDeviceToken,
294
298
  Notification,
295
299
  Coach,
296
300
  Scout,
@@ -44,6 +44,7 @@ export * from './wishlist';
44
44
  export * from './currency-transaction';
45
45
  export * from './user-pull-grant';
46
46
  export * from './user-settings';
47
+ export * from './user-device-token';
47
48
  export * from './notification';
48
49
  export * from './vper';
49
50
  export * from './views';
@@ -44,6 +44,7 @@ export * from './wishlist';
44
44
  export * from './currency-transaction';
45
45
  export * from './user-pull-grant';
46
46
  export * from './user-settings';
47
+ export * from './user-device-token';
47
48
  export * from './notification';
48
49
  export * from './vper';
49
50
  export * from './views';
@@ -0,0 +1,23 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ export interface UserDeviceTokenAttributes {
4
+ user_device_token_id: string;
5
+ user_id: string;
6
+ device_token: string;
7
+ platform: string;
8
+ created_at?: Date;
9
+ updated_at?: Date;
10
+ }
11
+ export type UserDeviceTokenPk = 'user_device_token_id';
12
+ export type UserDeviceTokenId = UserDeviceTokenModel[UserDeviceTokenPk];
13
+ export type UserDeviceTokenOptionalAttributes = 'user_device_token_id' | 'platform' | 'created_at' | 'updated_at';
14
+ export type UserDeviceTokenCreationAttributes = Optional<UserDeviceTokenAttributes, UserDeviceTokenOptionalAttributes>;
15
+ export declare class UserDeviceTokenModel extends Model<UserDeviceTokenAttributes, UserDeviceTokenCreationAttributes> implements UserDeviceTokenAttributes {
16
+ user_device_token_id: string;
17
+ user_id: string;
18
+ device_token: string;
19
+ platform: string;
20
+ created_at?: Date;
21
+ updated_at?: Date;
22
+ static initModel(sequelize: Sequelize.Sequelize): typeof UserDeviceTokenModel;
23
+ }
@@ -0,0 +1,61 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ export class UserDeviceTokenModel extends Model {
3
+ static initModel(sequelize) {
4
+ return UserDeviceTokenModel.init({
5
+ user_device_token_id: {
6
+ type: DataTypes.UUID,
7
+ allowNull: false,
8
+ primaryKey: true,
9
+ defaultValue: DataTypes.UUIDV4
10
+ },
11
+ user_id: {
12
+ type: DataTypes.UUID,
13
+ allowNull: false,
14
+ references: {
15
+ model: 'AuthUser',
16
+ key: 'user_id'
17
+ }
18
+ },
19
+ device_token: {
20
+ type: DataTypes.TEXT,
21
+ allowNull: false
22
+ },
23
+ platform: {
24
+ type: DataTypes.TEXT,
25
+ allowNull: false,
26
+ defaultValue: 'android'
27
+ },
28
+ created_at: {
29
+ type: DataTypes.DATE,
30
+ allowNull: false,
31
+ defaultValue: DataTypes.NOW
32
+ },
33
+ updated_at: {
34
+ type: DataTypes.DATE,
35
+ allowNull: false,
36
+ defaultValue: DataTypes.NOW
37
+ }
38
+ }, {
39
+ sequelize,
40
+ tableName: 'UserDeviceToken',
41
+ schema: 'public',
42
+ timestamps: false,
43
+ indexes: [
44
+ {
45
+ name: 'UserDeviceToken_pk',
46
+ unique: true,
47
+ fields: [{ name: 'user_device_token_id' }]
48
+ },
49
+ {
50
+ name: 'UserDeviceToken_user_id_device_token_uq',
51
+ unique: true,
52
+ fields: [{ name: 'user_id' }, { name: 'device_token' }]
53
+ },
54
+ {
55
+ name: 'UserDeviceToken_user_id_idx',
56
+ fields: [{ name: 'user_id' }]
57
+ }
58
+ ]
59
+ });
60
+ }
61
+ }
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from '@jest/globals';
2
- import { tacticsColumnsToApiDto } from './tactics';
3
- import { CourtPosition, EnergyBand } from '../../service';
2
+ import { tacticsColumnsToApiDto, transformFromTactics, transformToTactics } from './tactics';
3
+ import { CourtPosition, EnergyBand, RotationSystemEnum, Tactics } from '../../service';
4
+ import { makeCountry, makePlayer } from '../../service/test-helpers';
4
5
  // Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
5
6
  function makeTactics(designatedSubs) {
6
7
  return {
@@ -75,4 +76,64 @@ describe('tacticsColumnsToApiDto()', () => {
75
76
  expect(dto.secondLibero).toBe('reserve1');
76
77
  expect(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
77
78
  });
79
+ it('passes per-set tactics columns through to the API DTO shape', () => {
80
+ const model = makeTactics([]);
81
+ model.system_sets = [{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
82
+ model.offensive_preference_sets = [[{ rotation: 1, order: ['rf', 'mf'] }]];
83
+ const dto = tacticsColumnsToApiDto(model);
84
+ expect(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
85
+ expect(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, order: ['rf', 'mf'] }]]);
86
+ });
87
+ it('leaves the per-set DTO fields undefined when the columns are absent', () => {
88
+ const dto = tacticsColumnsToApiDto(makeTactics([]));
89
+ expect(dto.systemSets).toBeUndefined();
90
+ expect(dto.offensivePreferenceSets).toBeUndefined();
91
+ });
92
+ });
93
+ describe('per-set tactics — service <-> row round-trip', () => {
94
+ const country = makeCountry();
95
+ const p1 = makePlayer(country);
96
+ const p2 = makePlayer(country);
97
+ const p3 = makePlayer(country);
98
+ const p4 = makePlayer(country);
99
+ const p5 = makePlayer(country);
100
+ const p6 = makePlayer(country);
101
+ const roster = [p1, p2, p3, p4, p5, p6];
102
+ const tactics = Tactics.create({
103
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
104
+ liberoReplacements: [],
105
+ systemSets: [
106
+ { rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
107
+ { rotationSystem: RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
108
+ ],
109
+ offensivePreferenceSets: [[{ rotation: 1, order: [p2, p3] }]]
110
+ });
111
+ it('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
112
+ const attrs = transformFromTactics(tactics, 'team-1');
113
+ expect(attrs.system_sets).toEqual([
114
+ { rotationSystem: '5-1', designatedSetters: [p1.id] },
115
+ { rotationSystem: '6-0', designatedSetters: [] }
116
+ ]);
117
+ expect(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, order: [p2.id, p3.id] }]]);
118
+ });
119
+ it('transformToTactics resolves the stored ids back to roster players', () => {
120
+ const attrs = transformFromTactics(tactics, 'team-1');
121
+ const back = transformToTactics(attrs, roster);
122
+ expect(back.systemSets?.[0].rotationSystem).toBe(RotationSystemEnum.FIVE_ONE);
123
+ expect(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
124
+ expect(back.systemSets?.[1].designatedSetters).toEqual([]);
125
+ expect(back.offensivePreferenceSets?.[0][0].order.map(p => p.id)).toEqual([p2.id, p3.id]);
126
+ });
127
+ it('omits the per-set columns entirely for an all-sets config', () => {
128
+ const flat = Tactics.create({
129
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
130
+ liberoReplacements: []
131
+ });
132
+ const attrs = transformFromTactics(flat, 'team-1');
133
+ expect(attrs.system_sets).toBeUndefined();
134
+ expect(attrs.offensive_preference_sets).toBeUndefined();
135
+ const back = transformToTactics(attrs, roster);
136
+ expect(back.systemSets).toBeUndefined();
137
+ expect(back.offensivePreferenceSets).toBeUndefined();
138
+ });
78
139
  });
@@ -67,3 +67,118 @@ describe('TacticsInputSchema — second libero rules', () => {
67
67
  }
68
68
  });
69
69
  });
70
+ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferenceSets)', () => {
71
+ const country = makeCountry();
72
+ const p1 = makePlayer(country);
73
+ const p2 = makePlayer(country);
74
+ const p3 = makePlayer(country);
75
+ const p4 = makePlayer(country);
76
+ const p5 = makePlayer(country);
77
+ const p6 = makePlayer(country);
78
+ const reserve = makePlayer(country);
79
+ // Minimal valid flat input (6-0 default: no setters, no preferences). Zone layout: p1@1, p2@2, p3@3,
80
+ // p4@4, p5@5, p6@6 — opposite slot pairs (3 apart) are (1,4), (2,5), (3,6).
81
+ function baseInput(overrides = {}) {
82
+ return {
83
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
84
+ liberoReplacements: [],
85
+ ...overrides
86
+ };
87
+ }
88
+ it('accepts valid per-set overrides for both scopes', () => {
89
+ const res = TacticsInputSchema.safeParse(baseInput({
90
+ systemSets: [
91
+ { rotationSystem: '5-1', designatedSetters: [p1] },
92
+ { rotationSystem: '6-0', designatedSetters: [] },
93
+ { rotationSystem: '4-2', designatedSetters: [p1, p4] }
94
+ ],
95
+ offensivePreferenceSets: [
96
+ [{ rotation: 1, order: [p2, p3] }],
97
+ []
98
+ ]
99
+ }));
100
+ expect(res.success).toBe(true);
101
+ });
102
+ it('carries the per-set fields through Tactics.create parsing', () => {
103
+ const res = TacticsInputSchema.safeParse(baseInput({
104
+ systemSets: [{ rotationSystem: '5-1', designatedSetters: [p1] }],
105
+ offensivePreferenceSets: [[{ rotation: 2, order: [p3] }]]
106
+ }));
107
+ expect(res.success).toBe(true);
108
+ if (res.success) {
109
+ expect(res.data.systemSets?.[0].rotationSystem).toBe('5-1');
110
+ expect(res.data.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
111
+ expect(res.data.offensivePreferenceSets?.[0][0]).toMatchObject({ rotation: 2 });
112
+ }
113
+ });
114
+ it('rejects more than 5 per-set entries', () => {
115
+ const entry = { rotationSystem: '6-0', designatedSetters: [] };
116
+ expect(TacticsInputSchema.safeParse(baseInput({ systemSets: Array.from({ length: 6 }, () => entry) })).success).toBe(false);
117
+ expect(TacticsInputSchema.safeParse(baseInput({ offensivePreferenceSets: Array.from({ length: 6 }, () => []) })).success).toBe(false);
118
+ });
119
+ it('rejects a set entry whose setter count does not match its system (5-1 with none)', () => {
120
+ const res = TacticsInputSchema.safeParse(baseInput({
121
+ systemSets: [{ rotationSystem: '5-1', designatedSetters: [] }]
122
+ }));
123
+ expect(res.success).toBe(false);
124
+ if (!res.success) {
125
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTERS_COUNT_5-1_EXPECTED_1');
126
+ expect(issue?.path).toEqual(['systemSets', 0]);
127
+ }
128
+ });
129
+ it('rejects a set entry whose setter is not a court starter', () => {
130
+ const res = TacticsInputSchema.safeParse(baseInput({
131
+ systemSets: [
132
+ { rotationSystem: '6-0', designatedSetters: [] },
133
+ { rotationSystem: '5-1', designatedSetters: [reserve] }
134
+ ]
135
+ }));
136
+ expect(res.success).toBe(false);
137
+ if (!res.success) {
138
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTER_NOT_A_STARTER');
139
+ expect(issue?.path).toEqual(['systemSets', 1]);
140
+ }
141
+ });
142
+ it('rejects a 4-2 set entry whose setters are not in opposite slots', () => {
143
+ // p1@1 and p2@2 are adjacent (not 3 apart), so the 4-2 pair rule fails for that set entry.
144
+ const res = TacticsInputSchema.safeParse(baseInput({
145
+ systemSets: [{ rotationSystem: '4-2', designatedSetters: [p1, p2] }]
146
+ }));
147
+ expect(res.success).toBe(false);
148
+ if (!res.success) {
149
+ expect(res.error.issues.some(i => i.message === 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS')).toBe(true);
150
+ }
151
+ });
152
+ it('rejects a per-set preferences list with a duplicate rotation', () => {
153
+ const res = TacticsInputSchema.safeParse(baseInput({
154
+ offensivePreferenceSets: [[{ rotation: 1, order: [p2] }, { rotation: 1, order: [p3] }]]
155
+ }));
156
+ expect(res.success).toBe(false);
157
+ if (!res.success) {
158
+ const issue = res.error.issues.find(i => i.message === 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION');
159
+ expect(issue?.path).toEqual(['offensivePreferenceSets', 0]);
160
+ }
161
+ });
162
+ it('rejects a per-set preferences order that lists a non-starter', () => {
163
+ const res = TacticsInputSchema.safeParse(baseInput({
164
+ offensivePreferenceSets: [[{ rotation: 1, order: [reserve] }]]
165
+ }));
166
+ expect(res.success).toBe(false);
167
+ if (!res.success) {
168
+ expect(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
169
+ }
170
+ });
171
+ it('still validates the flat fields when per-set overrides are present', () => {
172
+ // Flat 5-1 with no setter is invalid even though every set entry is valid.
173
+ const res = TacticsInputSchema.safeParse(baseInput({
174
+ rotationSystem: '5-1',
175
+ designatedSetters: [],
176
+ systemSets: [{ rotationSystem: '6-0', designatedSetters: [] }]
177
+ }));
178
+ expect(res.success).toBe(false);
179
+ if (!res.success) {
180
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTERS_COUNT_5-1_EXPECTED_1');
181
+ expect(issue?.path).toEqual(['designatedSetters']);
182
+ }
183
+ });
184
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.459",
3
+ "version": "0.0.462",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",