@airoom/nextmin-node 0.1.2 → 0.1.4

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/LICENSE CHANGED
@@ -1,49 +1,21 @@
1
- Nextmin Proprietary License v1.0
1
+ MIT License
2
2
 
3
3
  Copyright (c) 2025 GSCodes
4
- All rights reserved.
5
4
 
6
- 1. Definitions
7
- “Software” means the package and all accompanying files under the package name(s) @airoom/nextmin-react and @airoom/nextmin-node, including any updates provided by Licensor.
8
- “Licensor” means GSCodes.
9
- “Licensee” means the person or entity that obtains the Software.
10
-
11
- 2. Grant of License
12
- Subject to full compliance with this License, Licensor grants Licensee a limited, non-exclusive, non-transferable, non-sublicensable license to:
13
- a) install and use the Software internally, solely for evaluating or running Licensee’s own applications; and
14
- b) make a reasonable number of copies solely for internal backup and archival purposes.
15
-
16
- 3. Restrictions
17
- Except as expressly permitted in Section 2, Licensee must NOT:
18
- a) copy, publish, distribute, sell, rent, lease, lend, sublicense, or otherwise make the Software available to any third party;
19
- b) modify, translate, adapt, create derivative works of, or merge the Software with other software;
20
- c) reverse engineer, decompile, disassemble, or otherwise attempt to derive source code or underlying ideas, except to the extent such restrictions are expressly prohibited by applicable law;
21
- d) remove, obscure, or alter any proprietary notices or markings; or
22
- e) use the Software to train, fine-tune, or improve any artificial intelligence or machine learning model.
23
-
24
- 4. Ownership
25
- The Software is licensed, not sold. Licensor retains all right, title, and interest in and to the Software, including all intellectual property rights and all copies.
26
-
27
- 5. Confidentiality
28
- The Software and any non-public information provided by Licensor shall be treated as confidential and not disclosed to any third party without Licensor’s prior written consent, except as required by law.
29
-
30
- 6. Term and Termination
31
- This License is effective until terminated. Licensor may terminate this License immediately upon notice if Licensee breaches any term. Upon termination, Licensee must cease all use and destroy all copies of the Software. Sections 3–10 survive termination.
32
-
33
- 7. Updates; No Support Obligation
34
- Licensor may, but is not obligated to, provide updates, bug fixes, or support. Any updates are governed by this License unless accompanied by a different license.
35
-
36
- 8. No Warranty
37
- THE SOFTWARE IS PROVIDED “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. USE IS AT LICENSEE’S SOLE RISK.
38
-
39
- 9. Limitation of Liability
40
- TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA, GOODWILL, OR BUSINESS INTERRUPTION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LICENSOR’S TOTAL LIABILITY FOR ALL CLAIMS SHALL NOT EXCEED THE AMOUNT PAID BY LICENSEE FOR THE SOFTWARE (IF ANY) IN THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
41
-
42
- 10. Governing Law; Venue
43
- This License is governed by the laws of State of California, USA, without regard to its conflict of laws principles. The parties submit to the exclusive jurisdiction and venue of the courts located in San Francisco County, California, USA.
44
-
45
- 11. Commercial Licensing
46
- For commercial use, redistribution, or other rights not expressly granted, contact: tareqaziz0065@gmail.com.
47
-
48
- 12. Entire Agreement
49
- This License constitutes the entire agreement with respect to the Software and supersedes all prior or contemporaneous agreements or understandings on the subject matter.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -375,6 +375,77 @@ class APIRouter {
375
375
  const salt = await bcrypt_1.default.genSalt(10);
376
376
  payload.password = await bcrypt_1.default.hash(String(payload.password) + this.jwtSecret, salt);
377
377
  }
378
+ // Extended schema handling: split into base + child
379
+ if (schema.extends) {
380
+ const baseName = String(schema.extends);
381
+ const baseLC = baseName.toLowerCase();
382
+ const baseSchema = this.getSchema(baseLC);
383
+ const baseModel = this.getModel(baseLC);
384
+ // Partition payload by base attributes
385
+ const basePayload = {};
386
+ const childPayload = {};
387
+ for (const [k, v] of Object.entries(payload)) {
388
+ if (baseSchema.attributes[k])
389
+ basePayload[k] = v;
390
+ else
391
+ childPayload[k] = v;
392
+ }
393
+ // Unique checks per collection
394
+ const baseConflicts = await this.checkUniqueFields(baseSchema, basePayload);
395
+ if (baseConflicts && baseConflicts.length > 0) {
396
+ return res.status(400).json({
397
+ error: true,
398
+ fields: baseConflicts.map((field) => ({
399
+ field,
400
+ error: true,
401
+ message: `You cannot use this ${field}. It's already been used.`,
402
+ })),
403
+ });
404
+ }
405
+ const childConflicts = await this.checkUniqueFields(schema, childPayload);
406
+ if (childConflicts && childConflicts.length > 0) {
407
+ return res.status(400).json({
408
+ error: true,
409
+ fields: childConflicts.map((field) => ({
410
+ field,
411
+ error: true,
412
+ message: `You cannot use this ${field}. It's already been used.`,
413
+ })),
414
+ });
415
+ }
416
+ // Hash password if base is Users
417
+ if (baseLC === 'users' && basePayload.password && this.jwtSecret) {
418
+ const salt = await bcrypt_1.default.genSalt(10);
419
+ basePayload.password = await bcrypt_1.default.hash(String(basePayload.password) + this.jwtSecret, salt);
420
+ }
421
+ // Create base first
422
+ const baseCreated = await baseModel.create(basePayload);
423
+ // Then create child linking to base
424
+ const linkField = 'baseId';
425
+ const childToCreate = { ...childPayload, [linkField]: baseCreated.id };
426
+ const childCreated = await model.create(childToCreate);
427
+ // Merge docs for response (child overrides base on conflicts)
428
+ let resultDoc = { ...baseCreated, ...childCreated };
429
+ delete resultDoc.baseId;
430
+ if (cdec.exposePrivate && childCreated?.id) {
431
+ // Re-fetch both with private if needed
432
+ const [refChild] = await model.read({ id: childCreated.id }, 1, 0, true);
433
+ const [refBase] = await baseModel.read({ id: baseCreated.id }, 1, 0, true);
434
+ if (refChild && refBase) {
435
+ resultDoc = { ...refBase, ...refChild };
436
+ delete resultDoc.baseId;
437
+ }
438
+ }
439
+ const masked = cdec.exposePrivate
440
+ ? (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.sensitiveMask)
441
+ : (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.readMask);
442
+ return res.status(201).json({
443
+ success: true,
444
+ message: `${schema.modelName} has been created successfully.`,
445
+ data: masked,
446
+ });
447
+ }
448
+ // Default (non-extended)
378
449
  const created = await model.create(payload);
379
450
  let resultDoc = created;
380
451
  if (cdec.exposePrivate && created?.id) {
@@ -577,11 +648,14 @@ class APIRouter {
577
648
  const totalRows = await model.count(finalFilter);
578
649
  // try DB-level sort (preferred). Fallback to in-memory if adapter lacks sort support.
579
650
  let rawRows = [];
651
+ // Ensure we can hydrate: baseId is private, so read with private when extended
652
+ const needPrivateForHydrate = !!schema.extends;
653
+ const exposePrivateForRead = needPrivateForHydrate || !!rdec.exposePrivate;
580
654
  try {
581
- rawRows = await model.read(finalFilter, limit + 1, page * limit, !!rdec.exposePrivate, { sort });
655
+ rawRows = await model.read(finalFilter, limit + 1, page * limit, exposePrivateForRead, { sort });
582
656
  }
583
657
  catch {
584
- rawRows = await model.read(finalFilter, limit + 1, page * limit, !!rdec.exposePrivate);
658
+ rawRows = await model.read(finalFilter, limit + 1, page * limit, exposePrivateForRead);
585
659
  if (sort && Object.keys(sort).length) {
586
660
  const orderKeys = Object.keys(sort);
587
661
  rawRows.sort((a, b) => {
@@ -611,6 +685,29 @@ class APIRouter {
611
685
  .filter((r) => String(r?.id) !== String(currentUserId))
612
686
  .slice(0, limit)
613
687
  : rawRows.slice(0, limit);
688
+ // If extended, hydrate with base data using baseId
689
+ if (schema.extends) {
690
+ const baseName = String(schema.extends);
691
+ const baseLC = baseName.toLowerCase();
692
+ const baseModel = this.getModel(baseLC);
693
+ const hydrated = [];
694
+ for (const row of rows) {
695
+ const baseId = row?.baseId;
696
+ if (baseId) {
697
+ const [baseDoc] = await baseModel.read({ id: baseId }, 1, 0, !!rdec.exposePrivate);
698
+ if (baseDoc) {
699
+ const merged = { ...baseDoc, ...row };
700
+ delete merged.baseId;
701
+ hydrated.push(merged);
702
+ continue;
703
+ }
704
+ }
705
+ const fallback = { ...row };
706
+ delete fallback.baseId;
707
+ hydrated.push(fallback);
708
+ }
709
+ rows = hydrated;
710
+ }
614
711
  const data = rdec.exposePrivate
615
712
  ? (0, authorize_1.applyReadMaskMany)(rows, rdec.sensitiveMask)
616
713
  : (0, authorize_1.applyReadMaskMany)(rows, rdec.readMask);
@@ -645,7 +742,10 @@ class APIRouter {
645
742
  access: schema.access,
646
743
  };
647
744
  const decForInclude = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, ctx);
648
- const recordArr = await model.read({ id: req.params.id }, 1, 0, !!decForInclude.exposePrivate);
745
+ // Ensure we can hydrate: baseId is private, so read with private when extended
746
+ const needPrivateForHydrate2 = !!schema.extends;
747
+ const exposePrivateForRead2 = needPrivateForHydrate2 || !!decForInclude.exposePrivate;
748
+ const recordArr = await model.read({ id: req.params.id }, 1, 0, exposePrivateForRead2);
649
749
  const doc = recordArr?.[0];
650
750
  if (!doc) {
651
751
  return res
@@ -655,9 +755,23 @@ class APIRouter {
655
755
  const dec = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, ctx, doc);
656
756
  if (!dec.allow)
657
757
  return res.status(403).json({ error: true, message: 'forbidden' });
758
+ let toReturn = doc;
759
+ if (schema.extends) {
760
+ const baseName = String(schema.extends);
761
+ const baseLC = baseName.toLowerCase();
762
+ const baseModel = this.getModel(baseLC);
763
+ const baseId = doc?.baseId;
764
+ if (baseId) {
765
+ const [baseDoc] = await baseModel.read({ id: baseId }, 1, 0, !!dec.exposePrivate);
766
+ if (baseDoc) {
767
+ toReturn = { ...baseDoc, ...doc };
768
+ delete toReturn.baseId;
769
+ }
770
+ }
771
+ }
658
772
  const data = dec.exposePrivate
659
- ? (0, authorize_1.applyReadMaskOne)(doc, dec.sensitiveMask)
660
- : (0, authorize_1.applyReadMaskOne)(doc, dec.readMask);
773
+ ? (0, authorize_1.applyReadMaskOne)(toReturn, dec.sensitiveMask)
774
+ : (0, authorize_1.applyReadMaskOne)(toReturn, dec.readMask);
661
775
  return res.status(200).json({
662
776
  success: true,
663
777
  message: `${schema.modelName} found`,
@@ -720,6 +834,83 @@ class APIRouter {
720
834
  message: `Missing required fields: ${missing.join(', ')}`,
721
835
  });
722
836
  }
837
+ // Extended schema update handling
838
+ if (schema.extends) {
839
+ const baseName = String(schema.extends);
840
+ const baseLC = baseName.toLowerCase();
841
+ const baseSchema = this.getSchema(baseLC);
842
+ const baseModel = this.getModel(baseLC);
843
+ const baseId = before?.baseId;
844
+ if (!baseId) {
845
+ return res.status(400).json({ error: true, message: 'Invalid extended record: missing baseId' });
846
+ }
847
+ // Partition update
848
+ const baseUpd = {};
849
+ const childUpd = {};
850
+ for (const [k, v] of Object.entries(upd)) {
851
+ if (k === 'baseId')
852
+ continue; // never allow changing link
853
+ if (baseSchema.attributes[k])
854
+ baseUpd[k] = v;
855
+ else
856
+ childUpd[k] = v;
857
+ }
858
+ // Unique checks per collection
859
+ const baseConflicts = await this.checkUniqueFields(baseSchema, baseUpd, String(baseId));
860
+ if (baseConflicts && baseConflicts.length > 0) {
861
+ return res.status(400).json({
862
+ error: true,
863
+ message: 'There are some error while updating record.',
864
+ fields: baseConflicts.map((field) => ({
865
+ field,
866
+ error: true,
867
+ message: `You cannot use this ${field}. It's already been used.`,
868
+ })),
869
+ });
870
+ }
871
+ const childConflicts = await this.checkUniqueFields(schema, childUpd, req.params.id);
872
+ if (childConflicts && childConflicts.length > 0) {
873
+ return res.status(400).json({
874
+ error: true,
875
+ message: 'There are some error while updating record.',
876
+ fields: childConflicts.map((field) => ({
877
+ field,
878
+ error: true,
879
+ message: `You cannot use this ${field}. It's already been used.`,
880
+ })),
881
+ });
882
+ }
883
+ // Hash password if base is Users and provided
884
+ if (baseLC === 'users' &&
885
+ Object.prototype.hasOwnProperty.call(baseUpd, 'password')) {
886
+ if (!baseUpd.password) {
887
+ delete baseUpd.password;
888
+ }
889
+ else if (this.jwtSecret) {
890
+ const salt = await bcrypt_1.default.genSalt(10);
891
+ baseUpd.password = await bcrypt_1.default.hash(String(baseUpd.password) + this.jwtSecret, salt);
892
+ }
893
+ }
894
+ // Perform updates
895
+ let updatedChild = before;
896
+ if (Object.keys(childUpd).length) {
897
+ updatedChild = await model.update(req.params.id, childUpd);
898
+ }
899
+ if (Object.keys(baseUpd).length) {
900
+ await baseModel.update(String(baseId), baseUpd);
901
+ }
902
+ // Re-read and merge
903
+ const [refChild] = await model.read({ id: updatedChild.id }, 1, 0, true);
904
+ const [refBase] = await baseModel.read({ id: String(baseId) }, 1, 0, true);
905
+ let responseDoc = refChild && refBase ? { ...refBase, ...refChild } : updatedChild;
906
+ if (responseDoc)
907
+ delete responseDoc.baseId;
908
+ const masked = udec.exposePrivate
909
+ ? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
910
+ : (0, authorize_1.applyReadMaskOne)(responseDoc, udec.readMask);
911
+ return res.json(masked);
912
+ }
913
+ // Default (non-extended)
723
914
  const conflicts = await this.checkUniqueFields(schema, upd, req.params.id);
724
915
  if (conflicts && conflicts.length > 0) {
725
916
  return res.status(400).json({
@@ -773,6 +964,36 @@ class APIRouter {
773
964
  const ddec = (0, authorize_1.authorize)(modelNameLC, 'delete', schemaPolicy, ctx, doc);
774
965
  if (!ddec.allow)
775
966
  return res.status(403).json({ error: true, message: 'forbidden' });
967
+ // Extended delete: remove child then base
968
+ if (schema.extends) {
969
+ const baseName = String(schema.extends);
970
+ const baseLC = baseName.toLowerCase();
971
+ const baseModel = this.getModel(baseLC);
972
+ const baseId = doc?.baseId;
973
+ const deletedChild = await model.delete(req.params.id);
974
+ if (baseId) {
975
+ try {
976
+ await baseModel.delete(String(baseId));
977
+ }
978
+ catch (e) {
979
+ // ignore if already missing
980
+ }
981
+ }
982
+ const merged = baseId
983
+ ? (() => {
984
+ // Try to include base snapshot for response if available
985
+ return { ...deletedChild, baseId: undefined };
986
+ })()
987
+ : deletedChild;
988
+ const masked = ddec.exposePrivate
989
+ ? (0, authorize_1.applyReadMaskOne)(merged, ddec.sensitiveMask)
990
+ : (0, authorize_1.applyReadMaskOne)(merged, ddec.readMask);
991
+ return res.json({
992
+ success: true,
993
+ message: `We have deleted the record successfully.`,
994
+ data: masked,
995
+ });
996
+ }
776
997
  const deletedRecord = await model.delete(req.params.id);
777
998
  if (!deletedRecord) {
778
999
  return res
@@ -975,6 +1196,31 @@ class APIRouter {
975
1196
  // fallback (BaseModel.read); note: no projection/sort
976
1197
  items = await this.getModel(targetLC).read({ id: { $in: ids } }, limit, skip, !!tDec.exposePrivate);
977
1198
  }
1199
+ // If extended, hydrate with base data using baseId before masking
1200
+ if (targetSchema.extends && Array.isArray(items) && items.length) {
1201
+ const baseName = String(targetSchema.extends);
1202
+ const baseLC = baseName.toLowerCase();
1203
+ const baseModel = this.getModel(baseLC);
1204
+ const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
1205
+ if (baseIds.length) {
1206
+ const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
1207
+ const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
1208
+ items = items.map((it) => {
1209
+ const bid = String(it?.baseId || '');
1210
+ const b = bid ? baseMap.get(bid) : null;
1211
+ const merged = b ? { ...b, ...it } : { ...it };
1212
+ delete merged.baseId;
1213
+ return merged;
1214
+ });
1215
+ }
1216
+ else {
1217
+ items = items.map((it) => {
1218
+ const copy = { ...it };
1219
+ delete copy.baseId;
1220
+ return copy;
1221
+ });
1222
+ }
1223
+ }
978
1224
  const masked = tDec.exposePrivate
979
1225
  ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask)
980
1226
  : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
@@ -1050,6 +1296,31 @@ class APIRouter {
1050
1296
  totalRows = all.length;
1051
1297
  items = all.slice(skip, skip + limit);
1052
1298
  }
1299
+ // If extended, hydrate with base data using baseId before masking
1300
+ if (targetSchema.extends && Array.isArray(items) && items.length) {
1301
+ const baseName = String(targetSchema.extends);
1302
+ const baseLC = baseName.toLowerCase();
1303
+ const baseModel = this.getModel(baseLC);
1304
+ const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
1305
+ if (baseIds.length) {
1306
+ const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
1307
+ const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
1308
+ items = items.map((it) => {
1309
+ const bid = String(it?.baseId || '');
1310
+ const b = bid ? baseMap.get(bid) : null;
1311
+ const merged = b ? { ...b, ...it } : { ...it };
1312
+ delete merged.baseId;
1313
+ return merged;
1314
+ });
1315
+ }
1316
+ else {
1317
+ items = items.map((it) => {
1318
+ const copy = { ...it };
1319
+ delete copy.baseId;
1320
+ return copy;
1321
+ });
1322
+ }
1323
+ }
1053
1324
  const masked = tDec.exposePrivate
1054
1325
  ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask)
1055
1326
  : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
@@ -1467,8 +1738,28 @@ class APIRouter {
1467
1738
  const isEmpty = (v) => v === undefined ||
1468
1739
  v === null ||
1469
1740
  (typeof v === 'string' && v.trim() === '');
1741
+ // If this is an extended schema, skip validating base fields here
1742
+ let baseKeys = null;
1743
+ const baseName = schema?.extends;
1744
+ if (baseName) {
1745
+ try {
1746
+ const baseLC = baseName.toLowerCase();
1747
+ const baseSchema = this.getSchema(baseLC);
1748
+ baseKeys = new Set(Object.keys(baseSchema.attributes || {}));
1749
+ }
1750
+ catch {
1751
+ baseKeys = null;
1752
+ }
1753
+ }
1470
1754
  for (const [key, attribute] of Object.entries(schema.attributes)) {
1471
- const req = attribute && !Array.isArray(attribute) && attribute.required;
1755
+ // Skip array defs here (arrays wrap inner type), and skip private fields entirely from client-required validation
1756
+ if (Array.isArray(attribute))
1757
+ continue;
1758
+ if (attribute?.private)
1759
+ continue;
1760
+ if (baseKeys && key !== 'baseId' && baseKeys.has(key))
1761
+ continue; // do not require base fields on child
1762
+ const req = Boolean(attribute?.required);
1472
1763
  if (!req)
1473
1764
  continue;
1474
1765
  if (mode === 'create') {
@@ -136,7 +136,19 @@ class MongoAdapter {
136
136
  /** Build a fresh Mongoose schema from our NextMin schema definition. */
137
137
  buildMongooseSchema(def) {
138
138
  const shape = {};
139
+ // If this schema extends a base, only include child-own attributes for storage
140
+ let baseKeys = null;
141
+ const baseName = def?.extends;
142
+ if (baseName) {
143
+ const baseSchema = this.getSchema(baseName);
144
+ if (baseSchema) {
145
+ baseKeys = new Set(Object.keys(baseSchema.attributes || {}));
146
+ }
147
+ }
139
148
  for (const [key, attr] of Object.entries(def.attributes)) {
149
+ // Exclude base attributes from child storage schema; always allow link field
150
+ if (baseKeys && key !== 'baseId' && baseKeys.has(key))
151
+ continue;
140
152
  shape[key] = this.mapAttribute(attr);
141
153
  }
142
154
  const s = new mongoose_1.Schema(shape, { timestamps: true });
@@ -19,6 +19,7 @@ export interface Attributes {
19
19
  }
20
20
  export interface Schema {
21
21
  modelName: string;
22
+ showCount: boolean;
22
23
  attributes: {
23
24
  [key: string]: Attributes;
24
25
  };
@@ -3,6 +3,7 @@ import { FieldIndexSpec } from '../database/DatabaseAdapter';
3
3
  type PublicAttributes = Record<string, any>;
4
4
  type PublicSchema = Omit<Schema, 'attributes'> & {
5
5
  modelName: string;
6
+ showCount: boolean;
6
7
  attributes: PublicAttributes;
7
8
  allowedMethods: Schema['allowedMethods'];
8
9
  };
@@ -125,8 +125,10 @@ class SchemaLoader {
125
125
  }
126
126
  for (const schema of Object.values(schemas)) {
127
127
  if (schema.extends) {
128
- const baseSchema = schemas[schema.extends];
128
+ const baseName = schema.extends;
129
+ const baseSchema = schemas[baseName];
129
130
  if (baseSchema) {
131
+ // Merge attributes and methods for READ surface
130
132
  schema.attributes = {
131
133
  ...baseSchema.attributes,
132
134
  ...schema.attributes,
@@ -135,6 +137,17 @@ class SchemaLoader {
135
137
  ...baseSchema.allowedMethods,
136
138
  ...schema.allowedMethods,
137
139
  };
140
+ // Inject hidden link field to base (used for storage join)
141
+ const linkField = 'baseId';
142
+ if (!schema.attributes[linkField]) {
143
+ schema.attributes[linkField] = {
144
+ type: 'ObjectId',
145
+ ref: baseName,
146
+ private: true,
147
+ // Not required on create; server auto-fills for extended creates.
148
+ // Presence is enforced for existing docs on update.
149
+ };
150
+ }
138
151
  }
139
152
  else {
140
153
  throw new Error(`Base schema ${schema.extends} not found for ${schema.modelName}`);
@@ -237,6 +250,7 @@ class SchemaLoader {
237
250
  };
238
251
  out[name] = {
239
252
  modelName: s.modelName,
253
+ showCount: s.showCount,
240
254
  allowedMethods: s.allowedMethods,
241
255
  attributes: attributesWithTimestamps,
242
256
  };
@@ -261,19 +275,29 @@ class SchemaLoader {
261
275
  if (Array.isArray(attr)) {
262
276
  const elem = attr[0];
263
277
  if (elem && typeof elem === 'object') {
264
- // Keep as an array with a single shallow-cloned descriptor, preserving flags like `private`, `sensitive`, `writeOnly`
265
- out[key] = [{ ...elem }];
278
+ // If the inner descriptor is private, omit this field entirely from public schema
279
+ if (elem.private) {
280
+ continue;
281
+ }
282
+ // Keep as an array with a single shallow-cloned descriptor (without leaking private flag)
283
+ const { private: _omit, ...rest } = elem;
284
+ out[key] = [{ ...rest }];
266
285
  }
267
286
  else {
268
- // Fallback: keep as-is
287
+ // Fallback: keep as-is (no private flag to check)
269
288
  out[key] = attr;
270
289
  }
271
290
  continue;
272
291
  }
273
292
  // Single attribute object
274
293
  if (attr && typeof attr === 'object') {
275
- // Keep `private`, `sensitive`, `writeOnly`, etc.
276
- out[key] = { ...attr };
294
+ // If marked private, omit from public schema entirely
295
+ if (attr.private) {
296
+ continue;
297
+ }
298
+ // Shallow clone and drop the private flag if present
299
+ const { private: _omit, ...rest } = attr;
300
+ out[key] = { ...rest };
277
301
  continue;
278
302
  }
279
303
  // Unexpected primitives — pass through
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-node",
3
- "version": "0.1.2",
4
- "license": "SEE LICENSE IN LICENSE",
3
+ "version": "0.1.4",
4
+ "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {